lomasub 0.1.3 → 0.2.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/README.md +24 -2
- package/cli.js +47 -13
- package/index.d.ts +16 -2
- package/index.js +58 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,10 +23,16 @@ lomasub transcribe video.mp4 --speaker-diarization --srt-max-chars 48 --srt-cue-
|
|
|
23
23
|
lomasub transcribe video.mp4 --config options.json
|
|
24
24
|
lomasub transcribe video.mp4 --no-ffmpeg
|
|
25
25
|
lomasub download <job-id> --output video.srt
|
|
26
|
+
lomasub transcribe video.mp4 --format txt # prints only the text, one line
|
|
27
|
+
lomasub transcribe video.mp4 --format txt -v # same, plus progress on stderr
|
|
28
|
+
lomasub transcribe video.mp4 --format txt -o video.txt
|
|
29
|
+
lomasub download <job-id> --format txt --speakers
|
|
26
30
|
lomasub --help
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
The command uploads the file, creates the job, waits, and saves `<input-stem>.srt` beside the input. `--output` changes the destination; `--overwrite` permits replacing an existing file. Progress and the job ID go to stderr; the saved path goes to stdout.
|
|
33
|
+
The command uploads the file, creates the job, waits, and saves `<input-stem>.srt` beside the input. `--output` changes the destination; `--overwrite` permits replacing an existing file. Progress and the job ID go to stderr; the saved path goes to stdout. `--output -` prints the result itself to stdout instead of saving it.
|
|
34
|
+
|
|
35
|
+
`--format txt` returns the transcript as plain text: every cue joined by a single space on one line, with no line breaks at all (not even at the end), no numbers, timings or markup. It is made for scripts: by default it prints **only** the text to stdout, with no progress lines and no file. `--output <file>` saves it instead (silently), `--verbose` / `-v` shows the progress, timing and saved path as SRT does, and `download <job-id> --format txt` needs no `--output`. Errors are always reported on stderr with exit code 1, naming the job ID so it can be fetched later with `download`. It follows the editor: once a subtitle is edited and saved, the text reflects the edit. Speaker labels (`A: ...`, from `--speaker-diarization`) are dropped unless `--speakers` is given. The default format is `srt`. If interrupted, the server job continues: resume using `download` and the printed job ID. Errors exit with code 1; cancellation exits with code 130. Job creation is never automatically retried.
|
|
30
36
|
|
|
31
37
|
When ffmpeg is on `PATH`, audio is extracted into a temporary Matroska audio file using `-map 0:a:0 -vn -c:a copy`. This copies the first audio stream without encoding, resampling, or changing quality. It excludes video to reduce uploads. If ffmpeg is missing or extraction fails (including a codec unsupported by Matroska), the original file is uploaded. `--no-ffmpeg` disables extraction. Temporary files are removed afterward.
|
|
32
38
|
|
|
@@ -81,10 +87,14 @@ Results include API job data and whichever artifact URLs the server produced. `t
|
|
|
81
87
|
| `waitForJob(jobId, options?)` | Poll until READY; throw with the failed stage on FAILED |
|
|
82
88
|
| `transcribeFile(file, options?)` | Submit, wait, optionally save SRT, and return timing |
|
|
83
89
|
| `downloadSrt(jobId, output, options?)` | Wait if needed, download original SRT, return absolute saved path |
|
|
90
|
+
| `downloadText(jobId, output, options?)` | Wait if needed, save the plain-text transcript, return absolute saved path |
|
|
91
|
+
| `getText(jobId, { signal, speakers }?)` | The plain-text transcript of a READY job as a string |
|
|
92
|
+
| `readResult(jobId, options?)` | Wait if needed and return the result as a string (`format`: `srt` or `txt`) |
|
|
93
|
+
| `resultText(status, options?)` | The result of a READY status (such as `transcribeFile`'s) as a string, without polling again |
|
|
84
94
|
|
|
85
95
|
Transcription options: `config`, `filename`, `language`, `duration_seconds`, `ffmpeg` (default `true`), `signal`, `onProgress`, and `pollIntervalMs` (default `2000`). Waiting/downloading accept `signal`, `onProgress`, and `pollIntervalMs`; downloading also accepts `overwrite` (default `false`). Output parent directories must already exist.
|
|
86
96
|
|
|
87
|
-
`transcribeFile()` additionally accepts `output` (
|
|
97
|
+
`transcribeFile()` additionally accepts `output` (result path), `format` (`srt` default, or `txt`), `speakers` (txt only, default `false`) and `overwrite` (default `false`). Existing output files are rejected before uploading unless overwrite is enabled.
|
|
88
98
|
|
|
89
99
|
After successful CLI transcription, stderr includes a final summary:
|
|
90
100
|
|
|
@@ -110,3 +120,15 @@ npm pack --dry-run
|
|
|
110
120
|
```
|
|
111
121
|
|
|
112
122
|
Tests use local mock HTTP servers. One optional ffmpeg test checks that stream-copy extraction preserves audio packet bytes. No production jobs are submitted.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
### Translation
|
|
126
|
+
|
|
127
|
+
Use one output mode, for example `--promodel translate-zh` for **Simplified Chinese**.
|
|
128
|
+
Other targets: `translate-th` (Thai), `translate-en` (English), `translate-ja`
|
|
129
|
+
(Japanese), `translate-ko` (Korean). `translate-jp` and `translate-kr` are aliases.
|
|
130
|
+
SDK: `config: { promodel: "translate-zh" }`. `language` still names the source
|
|
131
|
+
language. Translation combines conservative correction and translation in one
|
|
132
|
+
batched stage; it never runs `pro` first. Omit the option for original subtitles.
|
|
133
|
+
Use alignment `default` or `none`; `llmsplitting` is incompatible with translation.
|
|
134
|
+
Translated results have fixed cue timing, plain SRT/VTT, and no word-level karaoke.
|
package/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { lstat, readFile } from 'node:fs/promises';
|
|
3
3
|
import { extname, resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
|
-
import { LomaClient, TRANSCRIPTION_OPTIONS } from './index.js';
|
|
5
|
+
import { LomaClient, RESULT_FORMATS, TRANSCRIPTION_OPTIONS } from './index.js';
|
|
6
6
|
|
|
7
7
|
const options = {
|
|
8
8
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -10,6 +10,7 @@ const options = {
|
|
|
10
10
|
config: { type: 'string' }, language: { type: 'string' }, filename: { type: 'string' },
|
|
11
11
|
'duration-seconds': { type: 'string' },
|
|
12
12
|
'no-ffmpeg': { type: 'boolean' }, overwrite: { type: 'boolean' },
|
|
13
|
+
format: { type: 'string' }, speakers: { type: 'boolean' }, verbose: { type: 'boolean', short: 'v' },
|
|
13
14
|
};
|
|
14
15
|
for (const [key, type] of Object.entries(TRANSCRIPTION_OPTIONS)) {
|
|
15
16
|
const flag = key.replaceAll('_', '-');
|
|
@@ -20,7 +21,7 @@ for (const [key, type] of Object.entries(TRANSCRIPTION_OPTIONS)) {
|
|
|
20
21
|
// Display-only production defaults; omitted options still use the API's settings.
|
|
21
22
|
const DEFAULT_HELP = {
|
|
22
23
|
forcealigner_level: 'default; choices: default, llmsplitting, none',
|
|
23
|
-
promodel: 'disabled
|
|
24
|
+
promodel: 'disabled; pro or translate-th/en/ja/ko/zh (zh = Simplified Chinese; jp/kr aliases)',
|
|
24
25
|
min_vad_duration_seconds: '10',
|
|
25
26
|
max_vad_duration_seconds: '25',
|
|
26
27
|
vad_threshold: '0.30',
|
|
@@ -47,10 +48,15 @@ async function main() {
|
|
|
47
48
|
console.log(`Usage:
|
|
48
49
|
lomasub transcribe <file> [options]
|
|
49
50
|
lomasub download <job-id> --output <file.srt> [--overwrite]
|
|
51
|
+
lomasub download <job-id> --format txt [--output <file.txt>]
|
|
50
52
|
|
|
51
53
|
Set LOMA_API_SECRET_KEY before running. Default endpoint: https://api.lomasub.ai
|
|
52
54
|
Options:
|
|
53
|
-
--output, -o <path> Output
|
|
55
|
+
--output, -o <path> Output file (srt default: beside input); - prints to stdout
|
|
56
|
+
--format <srt|txt> srt (default) or txt: all cues on one line, joined by spaces.
|
|
57
|
+
txt prints only the text to stdout unless --output is given
|
|
58
|
+
--speakers txt only: keep speaker labels (A: ...)
|
|
59
|
+
--verbose, -v txt only: show progress and timing on stderr (srt always does)
|
|
54
60
|
--overwrite Replace existing output
|
|
55
61
|
--no-ffmpeg Upload original without audio extraction
|
|
56
62
|
--language <code> ASR language hint: th, en; server default: th
|
|
@@ -70,9 +76,15 @@ ${Object.entries(TRANSCRIPTION_OPTIONS).map(([key, type]) => {
|
|
|
70
76
|
if (!['transcribe', 'download'].includes(command) || !input || positionals.length !== 2) {
|
|
71
77
|
throw new Error('Use transcribe <file> or download <job-id>. See --help.');
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
79
|
+
const format = values.format ?? 'srt';
|
|
80
|
+
if (!RESULT_FORMATS.includes(format)) throw new Error(`--format must be one of: ${RESULT_FORMATS.join(', ')}.`);
|
|
81
|
+
if (values.speakers && format !== 'txt') throw new Error('--speakers requires --format txt.');
|
|
82
|
+
if (command === 'download' && !values.output && format !== 'txt') throw new Error('download requires --output.');
|
|
83
|
+
// txt is made for pipes: the text alone on stdout, nothing else unless --verbose.
|
|
84
|
+
const stdout = values.output === '-' || (format === 'txt' && values.output === undefined);
|
|
85
|
+
const verbose = format === 'srt' || values.verbose;
|
|
86
|
+
const output = stdout ? undefined : resolve(values.output ?? `${input.slice(0, input.length - extname(input).length)}.${format}`);
|
|
87
|
+
if (!stdout && !values.overwrite) {
|
|
76
88
|
try {
|
|
77
89
|
await lstat(output);
|
|
78
90
|
throw new Error(`Output exists: ${output}. Use --overwrite to replace it.`);
|
|
@@ -86,26 +98,46 @@ ${Object.entries(TRANSCRIPTION_OPTIONS).map(([key, type]) => {
|
|
|
86
98
|
if (values[`no-${flag}`]) config[key] = false;
|
|
87
99
|
else if (values[flag] !== undefined) config[key] = type === 'number' ? number(values[flag], flag) : values[flag];
|
|
88
100
|
}
|
|
101
|
+
let jobId = command === 'download' ? input : undefined;
|
|
89
102
|
const controller = new AbortController();
|
|
90
103
|
const cancel = () => controller.abort();
|
|
91
104
|
const settings = {
|
|
92
105
|
signal: controller.signal, overwrite: values.overwrite, ffmpeg: !values['no-ffmpeg'],
|
|
106
|
+
format, speakers: values.speakers,
|
|
93
107
|
language: values.language, filename: values.filename, config,
|
|
94
108
|
duration_seconds: values['duration-seconds'] === undefined ? undefined : number(values['duration-seconds'], 'duration-seconds'),
|
|
95
|
-
onProgress: ({ stage, job_id, state, message }) =>
|
|
109
|
+
onProgress: ({ stage, job_id, state, message }) => {
|
|
110
|
+
if (job_id) jobId = job_id;
|
|
111
|
+
if (verbose) console.error([stage, job_id, state, message].filter(Boolean).join(': '));
|
|
112
|
+
},
|
|
96
113
|
};
|
|
97
114
|
process.once('SIGINT', cancel);
|
|
98
115
|
process.once('SIGTERM', cancel);
|
|
99
116
|
try {
|
|
100
117
|
const client = new LomaClient({ endpoint: values.endpoint });
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
// stdout carries exactly the result (txt has no trailing newline); progress stays on stderr.
|
|
119
|
+
const print = text => new Promise((done, fail) => process.stdout.write(text, error => error ? fail(error) : done()));
|
|
120
|
+
if (command === 'download') {
|
|
121
|
+
if (stdout) await print(await client.readResult(input, settings));
|
|
122
|
+
else {
|
|
123
|
+
const saved = format === 'txt' ? await client.downloadText(input, output, settings) : await client.downloadSrt(input, output, settings);
|
|
124
|
+
if (verbose) console.log(saved);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
103
127
|
const result = await client.transcribeFile(input, { ...settings, output });
|
|
128
|
+
const text = stdout ? await client.resultText(result, settings) : undefined;
|
|
104
129
|
const { content_duration_seconds, total_seconds, speed_x } = result.timing;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
console.
|
|
130
|
+
const show = (value, unit) => value === null ? 'unknown' : `${value.toFixed(2)}${unit}`;
|
|
131
|
+
// Before the text: a txt result ends without a newline, so nothing may follow it.
|
|
132
|
+
if (verbose) console.error(`Content: ${show(content_duration_seconds, 's')} | Total: ${show(total_seconds, 's')} | Speed: ${show(speed_x, 'x realtime')}`);
|
|
133
|
+
if (stdout) await print(text);
|
|
134
|
+
else if (verbose) console.log(result.output_path);
|
|
108
135
|
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Quiet mode never printed the job id, so a cancel or failure names it for `download`.
|
|
138
|
+
error.jobId = jobId;
|
|
139
|
+
if (jobId && !verbose && error.name !== 'AbortError' && !error.message.includes(jobId)) error.message = `${error.message} (job ${jobId})`;
|
|
140
|
+
throw error;
|
|
109
141
|
} finally {
|
|
110
142
|
process.removeListener('SIGINT', cancel);
|
|
111
143
|
process.removeListener('SIGTERM', cancel);
|
|
@@ -113,6 +145,8 @@ ${Object.entries(TRANSCRIPTION_OPTIONS).map(([key, type]) => {
|
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
main().catch(error => {
|
|
116
|
-
console.error(error.name === 'AbortError'
|
|
148
|
+
console.error(error.name === 'AbortError'
|
|
149
|
+
? `Cancelled. An already-submitted job continues; resume with lomasub download ${error.jobId ?? '<job-id>'} --output <path>.`
|
|
150
|
+
: error.message);
|
|
117
151
|
process.exitCode = error.name === 'AbortError' ? 130 : 1;
|
|
118
152
|
});
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface TranscriptionConfig {
|
|
2
|
-
promodel?: 'pro' | null;
|
|
2
|
+
promodel?: 'pro' | 'translate-th' | 'translate-en' | 'translate-ja' | 'translate-ko' | 'translate-zh' | 'translate-jp' | 'translate-kr' | null;
|
|
3
3
|
forcealigner_level?: 'default' | 'llmsplitting' | 'none';
|
|
4
4
|
min_vad_duration_seconds?: number;
|
|
5
5
|
max_vad_duration_seconds?: number;
|
|
@@ -29,7 +29,16 @@ export interface WaitOptions {
|
|
|
29
29
|
pollIntervalMs?: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export
|
|
32
|
+
export type ResultFormat = 'srt' | 'txt';
|
|
33
|
+
|
|
34
|
+
export interface ResultOptions {
|
|
35
|
+
/** `srt` (default) or `txt`: every cue joined by one space, on a single line with no newline. */
|
|
36
|
+
format?: ResultFormat;
|
|
37
|
+
/** `txt` only: keep the diarization labels (`A: ...`). Default false. */
|
|
38
|
+
speakers?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TranscribeOptions extends WaitOptions, ResultOptions {
|
|
33
42
|
output?: string;
|
|
34
43
|
overwrite?: boolean;
|
|
35
44
|
ffmpeg?: boolean;
|
|
@@ -68,6 +77,7 @@ export interface TranscriptionResult extends JobStatus {
|
|
|
68
77
|
};
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
export const RESULT_FORMATS: readonly ResultFormat[];
|
|
71
81
|
export const TRANSCRIPTION_OPTIONS: Readonly<Record<string, 'string' | 'number' | 'boolean'>>;
|
|
72
82
|
|
|
73
83
|
export class LomaClient {
|
|
@@ -77,4 +87,8 @@ export class LomaClient {
|
|
|
77
87
|
waitForJob(jobId: string, options?: WaitOptions): Promise<JobStatus>;
|
|
78
88
|
transcribeFile(file: string, options?: TranscribeOptions): Promise<TranscriptionResult>;
|
|
79
89
|
downloadSrt(jobId: string, output: string, options?: WaitOptions & { overwrite?: boolean }): Promise<string>;
|
|
90
|
+
downloadText(jobId: string, output: string, options?: WaitOptions & { overwrite?: boolean; speakers?: boolean }): Promise<string>;
|
|
91
|
+
getText(jobId: string, options?: { signal?: AbortSignal; speakers?: boolean }): Promise<string>;
|
|
92
|
+
readResult(jobId: string, options?: WaitOptions & ResultOptions): Promise<string>;
|
|
93
|
+
resultText(status: JobStatus, options?: ResultOptions & Pick<WaitOptions, 'signal' | 'onProgress'>): Promise<string>;
|
|
80
94
|
}
|
package/index.js
CHANGED
|
@@ -48,6 +48,13 @@ async function checkedFetch(url, options, stage) {
|
|
|
48
48
|
return response;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export const RESULT_FORMATS = ['srt', 'txt'];
|
|
52
|
+
|
|
53
|
+
function resultFormat(format = 'srt') {
|
|
54
|
+
if (!RESULT_FORMATS.includes(format)) throw new Error(`format must be one of: ${RESULT_FORMATS.join(', ')}.`);
|
|
55
|
+
return format;
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
export class LomaClient {
|
|
52
59
|
constructor({ apiKey = process.env.LOMA_API_SECRET_KEY, endpoint = 'https://api.lomasub.ai' } = {}) {
|
|
53
60
|
if (!apiKey) throw new Error('Set LOMA_API_SECRET_KEY or provide apiKey.');
|
|
@@ -138,6 +145,7 @@ export class LomaClient {
|
|
|
138
145
|
|
|
139
146
|
async transcribeFile(file, options = {}) {
|
|
140
147
|
const started = performance.now();
|
|
148
|
+
resultFormat(options.format);
|
|
141
149
|
if (options.output !== undefined && !options.overwrite) {
|
|
142
150
|
try {
|
|
143
151
|
await lstat(options.output);
|
|
@@ -146,7 +154,7 @@ export class LomaClient {
|
|
|
146
154
|
}
|
|
147
155
|
const job = await this.submitFile(file, options);
|
|
148
156
|
const status = await this.waitForJob(job.job_id, options);
|
|
149
|
-
const output_path = options.output === undefined ? undefined : await this.#
|
|
157
|
+
const output_path = options.output === undefined ? undefined : await this.#save(status, options.output, options);
|
|
150
158
|
const total_seconds = (performance.now() - started) / 1000;
|
|
151
159
|
const content_duration_seconds = [status.audio_duration_seconds, options.duration_seconds]
|
|
152
160
|
.find(value => Number.isFinite(value) && value >= 0) ?? null;
|
|
@@ -158,18 +166,62 @@ export class LomaClient {
|
|
|
158
166
|
|
|
159
167
|
async downloadSrt(jobId, output, options = {}) {
|
|
160
168
|
const status = await this.waitForJob(jobId, options);
|
|
161
|
-
return this.#
|
|
169
|
+
return this.#save(status, output, { ...options, format: 'srt' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async downloadText(jobId, output, options = {}) {
|
|
173
|
+
const status = await this.waitForJob(jobId, options);
|
|
174
|
+
return this.#save(status, output, { ...options, format: 'txt' });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The current transcript of a READY job as one line: cues joined by a space, no newline.
|
|
178
|
+
async getText(jobId, { signal, speakers = false } = {}) {
|
|
179
|
+
return (await this.#fetchResult({ job_id: jobId }, 'txt', { signal, speakers })).text();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Wait for the job, then return its result as a string (`format`: 'srt' or 'txt').
|
|
183
|
+
async readResult(jobId, options = {}) {
|
|
184
|
+
resultFormat(options.format);
|
|
185
|
+
return this.resultText(await this.waitForJob(jobId, options), options);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The result of an already READY status (e.g. what transcribeFile returned) as a string.
|
|
189
|
+
async resultText(status, { format, speakers, signal, onProgress = () => {} } = {}) {
|
|
190
|
+
format = resultFormat(format);
|
|
191
|
+
onProgress({ stage: 'downloading', job_id: status.job_id });
|
|
192
|
+
return (await this.#fetchResult(status, format, { signal, speakers })).text();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async #fetchResult(status, format, { signal, speakers = false }) {
|
|
196
|
+
const jobId = status.job_id;
|
|
197
|
+
if (format === 'srt') {
|
|
198
|
+
if (!status.srt_url) throw new Error(`Job ${jobId} has no SRT URL.`);
|
|
199
|
+
return checkedFetch(status.srt_url, { signal }, 'Download');
|
|
200
|
+
}
|
|
201
|
+
// Text is derived by the API (it follows editor saves), so it needs the bearer key.
|
|
202
|
+
const query = speakers ? '?speakers=true' : '';
|
|
203
|
+
try {
|
|
204
|
+
return await checkedFetch(`${this.endpoint}/jobs/${encodeURIComponent(jobId)}.txt${query}`, {
|
|
205
|
+
signal, headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
206
|
+
}, 'Download');
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error.status === 404) {
|
|
209
|
+
error.message = `Job ${jobId} has no text transcript: it is not READY, not yours, or the server does not support .txt yet.`;
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
162
213
|
}
|
|
163
214
|
|
|
164
|
-
async #
|
|
215
|
+
async #save(status, output, { signal, overwrite = false, onProgress = () => {}, format, speakers }) {
|
|
216
|
+
format = resultFormat(format);
|
|
165
217
|
const jobId = status.job_id;
|
|
166
|
-
if (!status.srt_url) throw new Error(`Job ${jobId} has no SRT URL.`);
|
|
218
|
+
if (format === 'srt' && !status.srt_url) throw new Error(`Job ${jobId} has no SRT URL.`);
|
|
167
219
|
const target = resolve(output);
|
|
168
220
|
const temporary = await mkdtemp(join(dirname(target), '.lomasub-download-'));
|
|
169
|
-
const partial = join(temporary,
|
|
221
|
+
const partial = join(temporary, `result.${format}`);
|
|
170
222
|
try {
|
|
171
223
|
onProgress({ stage: 'downloading', job_id: jobId });
|
|
172
|
-
const response = await
|
|
224
|
+
const response = await this.#fetchResult(status, format, { signal, speakers });
|
|
173
225
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(partial), { signal });
|
|
174
226
|
signal?.throwIfAborted();
|
|
175
227
|
// link fails atomically if the destination exists; rename replaces it only when requested.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lomasub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lomasub cli is a command-line tool for creating subtitles from local audio or video in Node.js or the terminal, using lomasub.ai cloud service. ",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": { "node": ">=20" },
|