lomasub 0.1.0 → 0.1.2

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 CHANGED
@@ -2,17 +2,18 @@
2
2
 
3
3
  Create subtitles from a local audio or video file. Requires Node.js 20+ and a Loma Sub API key. No runtime npm dependencies.
4
4
 
5
+ Sign in to the editor, open your account dialog, and generate your personal API key. Copy it when shown: it is displayed only once. Each account has one active key; replacing or revoking it immediately invalidates the old key. Your CLI/SDK requests use your account's quota and create jobs in your own history. Inactive accounts cannot use personal keys. The shared deployment secret is reserved for internal scripts and workers.
6
+
5
7
  ## Install
6
8
 
7
- From this repository (the package has not been published to npm):
9
+ Run directly with npx:
8
10
 
9
11
  ```bash
10
- npm install --global ./cli-sdk
11
12
  export LOMA_API_SECRET_KEY="<your-api-key>"
12
- lomasub transcribe ./video.mp4 --language th
13
+ npx lomasub transcribe ./video.mp4 --language th
13
14
  ```
14
15
 
15
- For a Node.js project, run `npm install /path/to/loma_fastsrt/cli-sdk` instead.
16
+ For a Node.js project, run `npm install lomasub`. For a persistent CLI, run `npm install --global lomasub`. From a repository checkout, `npm install --global ./cli-sdk` also works.
16
17
 
17
18
  ## CLI
18
19
 
@@ -62,25 +63,38 @@ import { LomaClient } from 'lomasub';
62
63
 
63
64
  const client = new LomaClient(); // LOMA_API_SECRET_KEY; or { apiKey, endpoint }
64
65
  const result = await client.transcribeFile('./video.mp4', {
66
+ output: './video.srt',
65
67
  language: 'th',
66
68
  config: { speaker_diarization: true, srt_max_chars: 48 },
67
69
  onProgress: event => console.error(event),
68
70
  });
69
- await client.downloadSrt(result.job_id, './video.srt');
71
+ console.log(result.output_path, result.timing);
70
72
  ```
71
73
 
72
- Methods return plain API data; result URLs include whichever artifacts the server produced.
74
+ Results include API job data and whichever artifact URLs the server produced. `transcribeFile()` also returns timing and, when saved, `output_path`.
73
75
 
74
76
  | Method | Behavior |
75
77
  | --- | --- |
76
78
  | `submitFile(file, options?)` | Extract/upload and return the newly created job |
77
79
  | `getStatus(jobId, { signal }?)` | Read current status and fresh download URLs |
78
80
  | `waitForJob(jobId, options?)` | Poll until READY; throw with the failed stage on FAILED |
79
- | `transcribeFile(file, options?)` | Submit and wait |
81
+ | `transcribeFile(file, options?)` | Submit, wait, optionally save SRT, and return timing |
80
82
  | `downloadSrt(jobId, output, options?)` | Wait if needed, download original SRT, return absolute saved path |
81
83
 
82
84
  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.
83
85
 
86
+ `transcribeFile()` additionally accepts `output` (SRT path) and `overwrite` (default `false`). Existing output files are rejected before uploading unless overwrite is enabled.
87
+
88
+ After successful CLI transcription, stderr includes a final summary:
89
+
90
+ ```text
91
+ Content: 600.00s | Total: 60.00s | Speed: 10.00x realtime
92
+ ```
93
+
94
+ The SDK returns `timing: { content_duration_seconds, total_seconds, speed_x }`. Total time uses a monotonic clock and includes extraction, upload, queueing, processing, and saving the SRT when `output` is provided. Without `output`, timing ends at READY. Speed is content duration divided by total time: 10x means ten seconds of content per second elapsed. SDK numbers retain full precision; CLI values show two decimal places.
95
+
96
+ Content duration comes from the worker's measured `audio_duration_seconds`, falling back to your explicit `duration_seconds` hint. Missing duration/speed is `null` in the SDK and `unknown` in the CLI; zero duration is valid. Failures and cancellation produce no success summary. Standalone `download` does not report whole-run timing because it cannot measure the earlier local steps.
97
+
84
98
  Pass an `AbortController`'s `signal` to stop local work. A submitted job continues on the server. HTTP errors include `status` and `detail`; ordinary network/file errors propagate. There are no automatic retries.
85
99
 
86
100
  This package covers transcription and original results only. Account/login, job-history management, subtitle editing, and edited-subtitle uploads are outside its scope.
package/cli.js CHANGED
@@ -77,8 +77,14 @@ ${Object.entries(TRANSCRIPTION_OPTIONS).map(([key, type]) => ` --${key.replaceA
77
77
  process.once('SIGTERM', cancel);
78
78
  try {
79
79
  const client = new LomaClient({ endpoint: values.endpoint });
80
- const jobId = command === 'download' ? input : (await client.submitFile(input, settings)).job_id;
81
- console.log(await client.downloadSrt(jobId, output, settings));
80
+ if (command === 'download') console.log(await client.downloadSrt(input, output, settings));
81
+ else {
82
+ const result = await client.transcribeFile(input, { ...settings, output });
83
+ const { content_duration_seconds, total_seconds, speed_x } = result.timing;
84
+ const format = (value, unit) => value === null ? 'unknown' : `${value.toFixed(2)}${unit}`;
85
+ console.error(`Content: ${format(content_duration_seconds, 's')} | Total: ${format(total_seconds, 's')} | Speed: ${format(speed_x, 'x realtime')}`);
86
+ console.log(result.output_path);
87
+ }
82
88
  } finally {
83
89
  process.removeListener('SIGINT', cancel);
84
90
  process.removeListener('SIGTERM', cancel);
package/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export interface WaitOptions {
32
32
  }
33
33
 
34
34
  export interface TranscribeOptions extends WaitOptions {
35
+ output?: string;
36
+ overwrite?: boolean;
35
37
  ffmpeg?: boolean;
36
38
  config?: TranscriptionConfig;
37
39
  filename?: string;
@@ -47,6 +49,7 @@ export interface Job {
47
49
  }
48
50
 
49
51
  export interface JobStatus extends Job {
52
+ audio_duration_seconds?: number | null;
50
53
  config?: TranscriptionConfig;
51
54
  failed_stage?: string | null;
52
55
  error?: string | null;
@@ -58,6 +61,15 @@ export interface JobStatus extends Job {
58
61
  debug_vtt_url?: string;
59
62
  }
60
63
 
64
+ export interface TranscriptionResult extends JobStatus {
65
+ output_path?: string;
66
+ timing: {
67
+ content_duration_seconds: number | null;
68
+ total_seconds: number;
69
+ speed_x: number | null;
70
+ };
71
+ }
72
+
61
73
  export const TRANSCRIPTION_OPTIONS: Readonly<Record<string, 'string' | 'number' | 'boolean'>>;
62
74
 
63
75
  export class LomaClient {
@@ -65,6 +77,6 @@ export class LomaClient {
65
77
  submitFile(file: string, options?: TranscribeOptions): Promise<Job>;
66
78
  getStatus(jobId: string, options?: Pick<WaitOptions, 'signal'>): Promise<JobStatus>;
67
79
  waitForJob(jobId: string, options?: WaitOptions): Promise<JobStatus>;
68
- transcribeFile(file: string, options?: TranscribeOptions): Promise<JobStatus>;
80
+ transcribeFile(file: string, options?: TranscribeOptions): Promise<TranscriptionResult>;
69
81
  downloadSrt(jobId: string, output: string, options?: WaitOptions & { overwrite?: boolean }): Promise<string>;
70
82
  }
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { createReadStream, createWriteStream } from 'node:fs';
3
- import { link, mkdtemp, rename, rm, stat } from 'node:fs/promises';
3
+ import { link, lstat, mkdtemp, rename, rm, stat } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { basename, dirname, extname, join, resolve } from 'node:path';
6
6
  import { Readable } from 'node:stream';
@@ -134,13 +134,32 @@ export class LomaClient {
134
134
  }
135
135
 
136
136
  async transcribeFile(file, options = {}) {
137
+ const started = performance.now();
138
+ if (options.output !== undefined && !options.overwrite) {
139
+ try {
140
+ await lstat(options.output);
141
+ throw new Error(`Output exists: ${options.output}. Set overwrite: true to replace it.`);
142
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
143
+ }
137
144
  const job = await this.submitFile(file, options);
138
- return this.waitForJob(job.job_id, options);
145
+ const status = await this.waitForJob(job.job_id, options);
146
+ const output_path = options.output === undefined ? undefined : await this.#saveSrt(status, options.output, options);
147
+ const total_seconds = (performance.now() - started) / 1000;
148
+ const content_duration_seconds = [status.audio_duration_seconds, options.duration_seconds]
149
+ .find(value => Number.isFinite(value) && value >= 0) ?? null;
150
+ const speed = content_duration_seconds === null ? null : content_duration_seconds / total_seconds;
151
+ return { ...status, ...(output_path === undefined ? {} : { output_path }), timing: {
152
+ content_duration_seconds, total_seconds, speed_x: Number.isFinite(speed) ? speed : null,
153
+ } };
139
154
  }
140
155
 
141
156
  async downloadSrt(jobId, output, options = {}) {
142
- const { signal, overwrite = false, onProgress = () => {} } = options;
143
157
  const status = await this.waitForJob(jobId, options);
158
+ return this.#saveSrt(status, output, options);
159
+ }
160
+
161
+ async #saveSrt(status, output, { signal, overwrite = false, onProgress = () => {} }) {
162
+ const jobId = status.job_id;
144
163
  if (!status.srt_url) throw new Error(`Job ${jobId} has no SRT URL.`);
145
164
  const target = resolve(output);
146
165
  const temporary = await mkdtemp(join(dirname(target), '.lomasub-download-'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lomasub",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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" },