vidgen 0.0.1
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 +781 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/db/init.d.ts +6 -0
- package/dist/commands/db/init.js +18 -0
- package/dist/commands/generate/stt.d.ts +13 -0
- package/dist/commands/generate/stt.js +73 -0
- package/dist/commands/generate/tts.d.ts +11 -0
- package/dist/commands/generate/tts.js +30 -0
- package/dist/commands/index/chunk.d.ts +11 -0
- package/dist/commands/index/chunk.js +55 -0
- package/dist/commands/index/describe.d.ts +11 -0
- package/dist/commands/index/describe.js +29 -0
- package/dist/commands/index/embed.d.ts +11 -0
- package/dist/commands/index/embed.js +29 -0
- package/dist/commands/index/save.d.ts +10 -0
- package/dist/commands/index/save.js +24 -0
- package/dist/commands/project/create.d.ts +11 -0
- package/dist/commands/project/create.js +63 -0
- package/dist/commands/project/delete.d.ts +9 -0
- package/dist/commands/project/delete.js +31 -0
- package/dist/commands/project/get.d.ts +9 -0
- package/dist/commands/project/get.js +39 -0
- package/dist/commands/project/list.d.ts +6 -0
- package/dist/commands/project/list.js +26 -0
- package/dist/commands/project/validate.d.ts +10 -0
- package/dist/commands/project/validate.js +29 -0
- package/dist/commands/search/asset.d.ts +12 -0
- package/dist/commands/search/asset.js +35 -0
- package/dist/commands/segment/add.d.ts +11 -0
- package/dist/commands/segment/add.js +49 -0
- package/dist/commands/segment/delete.d.ts +9 -0
- package/dist/commands/segment/delete.js +29 -0
- package/dist/commands/segment/list.d.ts +9 -0
- package/dist/commands/segment/list.js +33 -0
- package/dist/commands/segment/update.d.ts +13 -0
- package/dist/commands/segment/update.js +50 -0
- package/dist/commands/video/caption.d.ts +12 -0
- package/dist/commands/video/caption.js +43 -0
- package/dist/commands/video/trim.d.ts +13 -0
- package/dist/commands/video/trim.js +52 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/db.d.ts +7 -0
- package/dist/lib/db.js +39 -0
- package/dist/lib/stt/deepgram-to-combo.d.ts +2 -0
- package/dist/lib/stt/deepgram-to-combo.js +49 -0
- package/dist/lib/stt/deepgram.d.ts +18 -0
- package/dist/lib/stt/deepgram.js +71 -0
- package/dist/lib/stt/detect-language.d.ts +2 -0
- package/dist/lib/stt/detect-language.js +31 -0
- package/dist/lib/stt/index.d.ts +50 -0
- package/dist/lib/stt/index.js +50 -0
- package/dist/lib/stt/types.d.ts +65 -0
- package/dist/lib/stt/types.js +1 -0
- package/dist/lib/types.d.ts +131 -0
- package/dist/lib/types.js +1 -0
- package/oclif.manifest.json +874 -0
- package/package.json +79 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
export default class SearchAsset extends Command {
|
|
3
|
+
static description = 'Semantically search the video RAG database for the best matching clip for a given query. Returns matching VisualBroll / Clip metadata.';
|
|
4
|
+
static examples = [
|
|
5
|
+
'<%= config.bin %> <%= command.id %> --query "bitcoin coin spinning on blue background" --db postgresql://user:pass@localhost/db --limit 3',
|
|
6
|
+
];
|
|
7
|
+
static flags = {
|
|
8
|
+
db: Flags.string({
|
|
9
|
+
char: 'd',
|
|
10
|
+
description: 'Database connection string',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
limit: Flags.integer({
|
|
14
|
+
char: 'l',
|
|
15
|
+
default: 1,
|
|
16
|
+
description: 'Maximum number of results to return',
|
|
17
|
+
}),
|
|
18
|
+
query: Flags.string({
|
|
19
|
+
char: 'q',
|
|
20
|
+
description: 'The natural language search query (e.g. comes from a Segment\'s searchQuery field)',
|
|
21
|
+
required: true,
|
|
22
|
+
}),
|
|
23
|
+
type: Flags.string({
|
|
24
|
+
char: 't',
|
|
25
|
+
description: 'Filter results by asset type',
|
|
26
|
+
options: ['video', 'image'],
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
async run() {
|
|
30
|
+
const { flags } = await this.parse(SearchAsset);
|
|
31
|
+
this.log(`[search:asset] Searching for "${flags.query}" (limit: ${flags.limit}) in database`);
|
|
32
|
+
// TODO: embed query, run vector similarity search, return top results as JSON
|
|
33
|
+
// Output shape: Array of VisualBroll { url, duration, type, time, ... }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class SegmentAdd extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
data: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
order: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
projectId: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { getDb } from '../../lib/db.js';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
export default class SegmentAdd extends Command {
|
|
5
|
+
static description = 'Add a new segment to a project. Data should be a JSON string matching the Segment interface.';
|
|
6
|
+
static examples = [
|
|
7
|
+
`<%= config.bin %> <%= command.id %> --projectId <project-id> --data '{"title":"Scene 1","text":"Hello"}' --order 0`,
|
|
8
|
+
];
|
|
9
|
+
static flags = {
|
|
10
|
+
data: Flags.string({
|
|
11
|
+
char: 'd',
|
|
12
|
+
description: 'JSON string of the segment data',
|
|
13
|
+
required: true,
|
|
14
|
+
}),
|
|
15
|
+
order: Flags.integer({
|
|
16
|
+
char: 'n',
|
|
17
|
+
default: 0,
|
|
18
|
+
description: 'Order index for the segment',
|
|
19
|
+
}),
|
|
20
|
+
projectId: Flags.string({
|
|
21
|
+
char: 'p',
|
|
22
|
+
description: 'Project ID to attach the segment to',
|
|
23
|
+
required: true,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
async run() {
|
|
27
|
+
const { flags } = await this.parse(SegmentAdd);
|
|
28
|
+
const id = uuidv4();
|
|
29
|
+
try {
|
|
30
|
+
// Validate JSON data can be parsed
|
|
31
|
+
const segmentObject = JSON.parse(flags.data);
|
|
32
|
+
// Ensure ID is set in the data too if not provided
|
|
33
|
+
if (!segmentObject.id) {
|
|
34
|
+
segmentObject.id = id;
|
|
35
|
+
}
|
|
36
|
+
await getDb().execute({
|
|
37
|
+
args: [segmentObject.id, flags.projectId, flags.order, JSON.stringify(segmentObject)],
|
|
38
|
+
sql: 'INSERT INTO segments (id, projectId, orderIndex, data) VALUES (?, ?, ?, ?)',
|
|
39
|
+
});
|
|
40
|
+
this.log(`Segment added successfully!`);
|
|
41
|
+
this.log(`ID: ${segmentObject.id}`);
|
|
42
|
+
// Output JSON for agent
|
|
43
|
+
this.log(JSON.stringify({ id: segmentObject.id, projectId: flags.projectId }));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.error(`Failed to add segment: ${error instanceof Error ? error.message : error}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class SegmentDelete extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { getDb } from '../../lib/db.js';
|
|
3
|
+
export default class SegmentDelete extends Command {
|
|
4
|
+
static args = {
|
|
5
|
+
id: Args.string({ description: 'Segment ID to delete', required: true }),
|
|
6
|
+
};
|
|
7
|
+
static description = 'Delete a specific segment from the Turso database.';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %> <segment-id>',
|
|
10
|
+
];
|
|
11
|
+
async run() {
|
|
12
|
+
const { args } = await this.parse(SegmentDelete);
|
|
13
|
+
try {
|
|
14
|
+
const rs = await getDb().execute({
|
|
15
|
+
args: [args.id],
|
|
16
|
+
sql: 'DELETE FROM segments WHERE id = ?',
|
|
17
|
+
});
|
|
18
|
+
if (rs.rowsAffected === 0) {
|
|
19
|
+
this.log(`Segment ${args.id} not found or already deleted.`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
this.log(`Segment ${args.id} deleted successfully.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
this.error(`Failed to delete segment: ${error instanceof Error ? error.message : error}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class SegmentList extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
projectId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { getDb } from '../../lib/db.js';
|
|
3
|
+
export default class SegmentList extends Command {
|
|
4
|
+
static args = {
|
|
5
|
+
projectId: Args.string({ description: 'Project ID to list segments for', required: true }),
|
|
6
|
+
};
|
|
7
|
+
static description = 'List all segments associated with a specific project.';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %> <project-id>',
|
|
10
|
+
];
|
|
11
|
+
async run() {
|
|
12
|
+
const { args } = await this.parse(SegmentList);
|
|
13
|
+
try {
|
|
14
|
+
const rs = await getDb().execute({
|
|
15
|
+
args: [args.projectId],
|
|
16
|
+
sql: 'SELECT id, orderIndex, data FROM segments WHERE projectId = ? ORDER BY orderIndex ASC',
|
|
17
|
+
});
|
|
18
|
+
if (rs.rows.length === 0) {
|
|
19
|
+
this.log('No segments found for this project.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.log(`Listing segments for project ${args.projectId}:`);
|
|
23
|
+
for (const row of rs.rows) {
|
|
24
|
+
const data = JSON.parse(row.data);
|
|
25
|
+
this.log(`[${row.orderIndex}] ${data.title || 'Untitled'} (${row.id})`);
|
|
26
|
+
}
|
|
27
|
+
this.log(JSON.stringify(rs.rows.map((r) => ({ id: r.id, orderIndex: r.orderIndex, ...JSON.parse(r.data) }))));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
this.error(`Failed to list segments: ${error instanceof Error ? error.message : error}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class SegmentUpdate extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
data: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
order: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { getDb } from '../../lib/db.js';
|
|
3
|
+
export default class SegmentUpdate extends Command {
|
|
4
|
+
static args = {
|
|
5
|
+
id: Args.string({ description: 'Segment ID to update', required: true }),
|
|
6
|
+
};
|
|
7
|
+
static description = 'Update an existing segment\'s data.';
|
|
8
|
+
static examples = [
|
|
9
|
+
`<%= config.bin %> <%= command.id %> <segment-id> --data '{"title":"New Title"}'`,
|
|
10
|
+
];
|
|
11
|
+
static flags = {
|
|
12
|
+
data: Flags.string({
|
|
13
|
+
char: 'd',
|
|
14
|
+
description: 'JSON string of the segment data to merge/update',
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
order: Flags.integer({
|
|
18
|
+
char: 'n',
|
|
19
|
+
description: 'New order index for the segment',
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(SegmentUpdate);
|
|
24
|
+
try {
|
|
25
|
+
// Get existing data
|
|
26
|
+
const rs = await getDb().execute({
|
|
27
|
+
args: [args.id],
|
|
28
|
+
sql: 'SELECT data, orderIndex FROM segments WHERE id = ?',
|
|
29
|
+
});
|
|
30
|
+
if (rs.rows.length === 0) {
|
|
31
|
+
this.error(`Segment with ID ${args.id} not found.`);
|
|
32
|
+
}
|
|
33
|
+
const existingData = JSON.parse(rs.rows[0].data);
|
|
34
|
+
const updateData = JSON.parse(flags.data);
|
|
35
|
+
const newData = {
|
|
36
|
+
...existingData,
|
|
37
|
+
...updateData,
|
|
38
|
+
};
|
|
39
|
+
const orderIndex = flags.order !== undefined ? flags.order : rs.rows[0].orderIndex;
|
|
40
|
+
await getDb().execute({
|
|
41
|
+
args: [orderIndex, JSON.stringify(newData), args.id],
|
|
42
|
+
sql: 'UPDATE segments SET orderIndex = ?, data = ? WHERE id = ?',
|
|
43
|
+
});
|
|
44
|
+
this.log(`Segment ${args.id} updated successfully.`);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
this.error(`Failed to update segment: ${error instanceof Error ? error.message : error}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class VideoSubtitle extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
burnIn: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
transcript: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
video: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
private formatTime;
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
export default class VideoSubtitle extends Command {
|
|
7
|
+
static description = 'Generate SRT captions from a JSON transcript and optionally embed them into a video.';
|
|
8
|
+
static flags = {
|
|
9
|
+
burnIn: Flags.boolean({ char: 'b' }),
|
|
10
|
+
output: Flags.string({ char: 'o', required: true }),
|
|
11
|
+
transcript: Flags.string({ char: 't', required: true }),
|
|
12
|
+
video: Flags.string({ char: 'v' }),
|
|
13
|
+
};
|
|
14
|
+
formatTime(seconds) {
|
|
15
|
+
const date = new Date(seconds * 1000);
|
|
16
|
+
return date.toISOString().substr(11, 12).replace('.', ',');
|
|
17
|
+
}
|
|
18
|
+
async run() {
|
|
19
|
+
const { flags } = await this.parse(VideoSubtitle);
|
|
20
|
+
const transcript = JSON.parse(await fs.readFile(flags.transcript, 'utf8'));
|
|
21
|
+
const words = transcript.results?.channels?.[0]?.alternatives?.[0]?.words || [];
|
|
22
|
+
let srtContent = '';
|
|
23
|
+
let group = [];
|
|
24
|
+
let index = 1;
|
|
25
|
+
for (let i = 0; i < words.length; i++) {
|
|
26
|
+
group.push(words[i]);
|
|
27
|
+
if (group.length >= 7 || i === words.length - 1) {
|
|
28
|
+
srtContent += `${index}\n${this.formatTime(group[0].start)} --> ${this.formatTime(group[group.length - 1].end)}\n${group.map(w => w.punctuated_word || w.word).join(' ')}\n\n`;
|
|
29
|
+
index++;
|
|
30
|
+
group = [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const srtPath = flags.video ? `${flags.transcript}.srt` : flags.output;
|
|
34
|
+
await fs.writeFile(srtPath, srtContent);
|
|
35
|
+
if (flags.video) {
|
|
36
|
+
let cmd = flags.burnIn
|
|
37
|
+
? `ffmpeg -i "${flags.video}" -vf "subtitles='${srtPath.replace(/:/g, '\\:')}'" "${flags.output}" -y`
|
|
38
|
+
: `ffmpeg -i "${flags.video}" -i "${srtPath}" -c copy -c:s mov_text "${flags.output}" -y`;
|
|
39
|
+
await execAsync(cmd);
|
|
40
|
+
}
|
|
41
|
+
this.log(JSON.stringify({ path: flags.video ? flags.output : srtPath }));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class VideoTrim extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
duration: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
end: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
input: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
start: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
export default class VideoTrim extends Command {
|
|
6
|
+
static description = 'Precisely trim a video file using start and end timestamps.';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> <%= command.id %> --input input.mp4 --start 00:00:10 --end 00:00:20 --output trimmed.mp4',
|
|
9
|
+
];
|
|
10
|
+
static flags = {
|
|
11
|
+
duration: Flags.string({
|
|
12
|
+
char: 'd',
|
|
13
|
+
description: 'Duration to trim',
|
|
14
|
+
}),
|
|
15
|
+
end: Flags.string({
|
|
16
|
+
char: 'e',
|
|
17
|
+
description: 'End timestamp',
|
|
18
|
+
}),
|
|
19
|
+
input: Flags.string({
|
|
20
|
+
char: 'i',
|
|
21
|
+
description: 'Input video file path',
|
|
22
|
+
required: true,
|
|
23
|
+
}),
|
|
24
|
+
output: Flags.string({
|
|
25
|
+
char: 'o',
|
|
26
|
+
description: 'Output video file path',
|
|
27
|
+
required: true,
|
|
28
|
+
}),
|
|
29
|
+
start: Flags.string({
|
|
30
|
+
char: 's',
|
|
31
|
+
description: 'Start timestamp',
|
|
32
|
+
required: true,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
async run() {
|
|
36
|
+
const { flags } = await this.parse(VideoTrim);
|
|
37
|
+
let cmd = `ffmpeg -i "${flags.input}" -ss ${flags.start}`;
|
|
38
|
+
if (flags.end)
|
|
39
|
+
cmd += ` -to ${flags.end}`;
|
|
40
|
+
else if (flags.duration)
|
|
41
|
+
cmd += ` -t ${flags.duration}`;
|
|
42
|
+
cmd += ` -c copy "${flags.output}" -y`;
|
|
43
|
+
try {
|
|
44
|
+
await execAsync(cmd);
|
|
45
|
+
this.log(`[video:trim] Done.`);
|
|
46
|
+
this.log(JSON.stringify({ path: flags.output }));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.error(`FFmpeg failed: ${error instanceof Error ? error.message : error}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
package/dist/lib/db.d.ts
ADDED
package/dist/lib/db.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createClient } from '@libsql/client';
|
|
2
|
+
let internalDb = null;
|
|
3
|
+
function getDb() {
|
|
4
|
+
if (internalDb)
|
|
5
|
+
return internalDb;
|
|
6
|
+
const url = process.env.TURSO_DATABASE_URL;
|
|
7
|
+
const authToken = process.env.TURSO_AUTH_TOKEN;
|
|
8
|
+
if (!url || !authToken) {
|
|
9
|
+
throw new Error('TURSO_DATABASE_URL and TURSO_AUTH_TOKEN must be set');
|
|
10
|
+
}
|
|
11
|
+
internalDb = createClient({
|
|
12
|
+
url,
|
|
13
|
+
authToken,
|
|
14
|
+
});
|
|
15
|
+
return internalDb;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the database tables if they don't exist.
|
|
19
|
+
*/
|
|
20
|
+
export async function initDb() {
|
|
21
|
+
const db = getDb();
|
|
22
|
+
await db.batch([
|
|
23
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
title TEXT NOT NULL,
|
|
26
|
+
description TEXT,
|
|
27
|
+
data TEXT, -- JSON blob
|
|
28
|
+
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
29
|
+
);`,
|
|
30
|
+
`CREATE TABLE IF NOT EXISTS segments (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
projectId TEXT NOT NULL,
|
|
33
|
+
orderIndex INTEGER NOT NULL,
|
|
34
|
+
data TEXT, -- JSON blob
|
|
35
|
+
FOREIGN KEY (projectId) REFERENCES projects(id) ON DELETE CASCADE
|
|
36
|
+
);`,
|
|
37
|
+
], "write");
|
|
38
|
+
}
|
|
39
|
+
export { getDb };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { detectLanguage } from './detect-language.js';
|
|
2
|
+
const getWords = (deepgramResult) => {
|
|
3
|
+
const words = deepgramResult.results.channels[0].alternatives[0].words.map((w) => {
|
|
4
|
+
return {
|
|
5
|
+
word: w.punctuated_word,
|
|
6
|
+
start: w.start,
|
|
7
|
+
end: w.end,
|
|
8
|
+
confidence: w.confidence,
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
return words;
|
|
12
|
+
};
|
|
13
|
+
const getParagraphs = (deepgramResult) => {
|
|
14
|
+
const paragraphs = deepgramResult.results.channels[0].alternatives[0].paragraphs.paragraphs
|
|
15
|
+
.map((p) => {
|
|
16
|
+
return {
|
|
17
|
+
sentences: p.sentences.map((s) => {
|
|
18
|
+
return {
|
|
19
|
+
text: s.text,
|
|
20
|
+
start: s.start,
|
|
21
|
+
end: s.end,
|
|
22
|
+
};
|
|
23
|
+
}),
|
|
24
|
+
numWords: p.num_words,
|
|
25
|
+
start: p.start,
|
|
26
|
+
end: p.end,
|
|
27
|
+
};
|
|
28
|
+
})
|
|
29
|
+
.filter((p) => p.sentences.length > 0);
|
|
30
|
+
return paragraphs;
|
|
31
|
+
};
|
|
32
|
+
export async function deepgramToCombo(deepgramResult) {
|
|
33
|
+
const text = deepgramResult.results.channels[0].alternatives[0].transcript;
|
|
34
|
+
const language = await detectLanguage(text);
|
|
35
|
+
const words = getWords(deepgramResult);
|
|
36
|
+
const duration = deepgramResult.metadata.duration;
|
|
37
|
+
const paragraphs = getParagraphs(deepgramResult);
|
|
38
|
+
return {
|
|
39
|
+
duration,
|
|
40
|
+
results: {
|
|
41
|
+
main: {
|
|
42
|
+
language,
|
|
43
|
+
paragraphs,
|
|
44
|
+
text,
|
|
45
|
+
words,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TranscriptObject } from './types.js';
|
|
2
|
+
export declare class SttService {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private params;
|
|
5
|
+
private url;
|
|
6
|
+
constructor(url: string, apiKey: string, model: string);
|
|
7
|
+
transcribe(audioUrl: string): Promise<{
|
|
8
|
+
transcript: Partial<TranscriptObject>;
|
|
9
|
+
duration: number;
|
|
10
|
+
}>;
|
|
11
|
+
transcribeV2(audioUrl: string): Promise<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
data: {
|
|
14
|
+
transcript: Partial<TranscriptObject>;
|
|
15
|
+
duration: number;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { deepgramToCombo } from './deepgram-to-combo.js';
|
|
2
|
+
export class SttService {
|
|
3
|
+
apiKey;
|
|
4
|
+
params;
|
|
5
|
+
url;
|
|
6
|
+
constructor(url, apiKey, model) {
|
|
7
|
+
this.url = url;
|
|
8
|
+
this.apiKey = apiKey;
|
|
9
|
+
this.params = new URLSearchParams({
|
|
10
|
+
model,
|
|
11
|
+
smart_format: 'true',
|
|
12
|
+
filler_words: 'false',
|
|
13
|
+
punctuate: 'true',
|
|
14
|
+
detect_language: 'true',
|
|
15
|
+
}).toString();
|
|
16
|
+
}
|
|
17
|
+
async transcribe(audioUrl) {
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(`${this.url}/listen?${this.params}`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Token ${this.apiKey}`,
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({ url: audioUrl }),
|
|
26
|
+
});
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
console.error(data);
|
|
30
|
+
throw new Error(`Transcription failed - ${response.status}`);
|
|
31
|
+
}
|
|
32
|
+
const caption_combo = await deepgramToCombo(data);
|
|
33
|
+
return { transcript: caption_combo, duration: data.metadata.duration };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error(err);
|
|
37
|
+
throw new Error('An unknown error occurred.');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async transcribeV2(audioUrl) {
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`${this.url}/listen?${this.params}`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Token ${this.apiKey}`,
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({ url: audioUrl }),
|
|
49
|
+
});
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
console.error(data);
|
|
53
|
+
return { success: false, data: { transcript: {}, duration: 0 } };
|
|
54
|
+
}
|
|
55
|
+
const caption_combo = await deepgramToCombo(data);
|
|
56
|
+
const durationNum = typeof data.metadata.duration === 'number'
|
|
57
|
+
? data.metadata.duration
|
|
58
|
+
: parseFloat(data.metadata.duration);
|
|
59
|
+
if (isNaN(durationNum))
|
|
60
|
+
throw new Error(`Invalid duration for ${audioUrl}`);
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
data: { transcript: caption_combo, duration: durationNum },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(err);
|
|
68
|
+
return { success: false, data: { transcript: {}, duration: 0 } };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { detectAll } from 'tinyld';
|
|
2
|
+
export async function detectLanguage(text) {
|
|
3
|
+
const tinyldResults = detectAll(text);
|
|
4
|
+
const results = tinyldResults.map((result) => ({
|
|
5
|
+
language: result.lang,
|
|
6
|
+
languageName: languageCodeToName(result.lang),
|
|
7
|
+
confidence: result.accuracy,
|
|
8
|
+
}));
|
|
9
|
+
const [mainLanguage] = results;
|
|
10
|
+
if (!mainLanguage) {
|
|
11
|
+
return {
|
|
12
|
+
language: 'en',
|
|
13
|
+
languageName: 'English',
|
|
14
|
+
confidence: 1,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (mainLanguage.language === 'und') {
|
|
18
|
+
mainLanguage.language = 'en';
|
|
19
|
+
mainLanguage.languageName = 'English';
|
|
20
|
+
}
|
|
21
|
+
return mainLanguage;
|
|
22
|
+
}
|
|
23
|
+
export function languageCodeToName(languageCode) {
|
|
24
|
+
const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });
|
|
25
|
+
let translatedLanguageName;
|
|
26
|
+
try {
|
|
27
|
+
translatedLanguageName = languageNames.of(languageCode);
|
|
28
|
+
}
|
|
29
|
+
catch (e) { }
|
|
30
|
+
return translatedLanguageName || 'Unknown';
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { TranscriptObject } from './types.js';
|
|
2
|
+
export interface TranscribeOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Audio URL to transcribe
|
|
5
|
+
*/
|
|
6
|
+
url: string;
|
|
7
|
+
/**
|
|
8
|
+
* API key for Deepgram (optional, defaults to env variable)
|
|
9
|
+
*/
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Target language for transcription (optional)
|
|
13
|
+
* If not provided, will auto-detect
|
|
14
|
+
*/
|
|
15
|
+
language?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Deepgram model to use (optional, defaults to "nova-3")
|
|
18
|
+
*/
|
|
19
|
+
model?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to enable smart formatting (optional, defaults to true)
|
|
22
|
+
*/
|
|
23
|
+
smartFormat?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to include paragraphs in the result (optional, defaults to true)
|
|
26
|
+
*/
|
|
27
|
+
paragraphs?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to include word-level timestamps (optional, defaults to true)
|
|
30
|
+
*/
|
|
31
|
+
words?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Transcribe audio from a URL using Deepgram
|
|
35
|
+
*
|
|
36
|
+
* @param options - Transcription options
|
|
37
|
+
* @returns Parsed transcription result in Combo format
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const result = await transcribe({
|
|
42
|
+
* url: "https://example.com/audio.mp3",
|
|
43
|
+
* language: "en"
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function transcribe(options: TranscribeOptions): Promise<Partial<TranscriptObject>>;
|
|
48
|
+
export * from './types.js';
|
|
49
|
+
export { deepgramToCombo } from './deepgram-to-combo.js';
|
|
50
|
+
export { detectLanguage } from './detect-language.js';
|