faces-cli 1.4.6 → 1.5.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.
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../base.js';
|
|
3
3
|
import { FacesAPIError } from '../../client.js';
|
|
4
|
+
function sleep(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
4
7
|
export default class CompileImport extends BaseCommand {
|
|
5
8
|
static description = 'Import a YouTube video into a face via server-side download and transcription';
|
|
6
9
|
static flags = {
|
|
@@ -29,6 +32,7 @@ export default class CompileImport extends BaseCommand {
|
|
|
29
32
|
async run() {
|
|
30
33
|
const { args, flags } = await this.parse(CompileImport);
|
|
31
34
|
const client = this.makeClient(flags);
|
|
35
|
+
const json = this.jsonEnabled();
|
|
32
36
|
if (flags['face-speaker'] && flags.type !== 'thread') {
|
|
33
37
|
this.error('--face-speaker is only valid with --type thread');
|
|
34
38
|
}
|
|
@@ -39,7 +43,8 @@ export default class CompileImport extends BaseCommand {
|
|
|
39
43
|
};
|
|
40
44
|
if (flags['face-speaker'])
|
|
41
45
|
payload.face_speaker = flags['face-speaker'];
|
|
42
|
-
|
|
46
|
+
if (!json)
|
|
47
|
+
process.stderr.write('Importing (downloading + transcribing in background)...\n');
|
|
43
48
|
let data;
|
|
44
49
|
try {
|
|
45
50
|
data = await client.post(`/v1/faces/${args.face_id}/import`, { body: payload });
|
|
@@ -53,26 +58,56 @@ export default class CompileImport extends BaseCommand {
|
|
|
53
58
|
}
|
|
54
59
|
throw err;
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
// Poll until transcription completes
|
|
62
|
+
if (data.prepare_status === 'transcribing') {
|
|
63
|
+
const id = (data.thread_id ?? data.document_id);
|
|
64
|
+
const endpoint = flags.type === 'thread'
|
|
65
|
+
? `/v1/compile/threads/${id}`
|
|
66
|
+
: `/v1/compile/documents/${id}`;
|
|
67
|
+
const timeout = 3600_000; // 1 hour
|
|
68
|
+
const start = Date.now();
|
|
69
|
+
while (Date.now() - start < timeout) {
|
|
70
|
+
await sleep(5000);
|
|
71
|
+
try {
|
|
72
|
+
const poll = await client.get(endpoint);
|
|
73
|
+
const status = poll.prepare_status;
|
|
74
|
+
if (status !== 'transcribing') {
|
|
75
|
+
data = poll;
|
|
76
|
+
if (!json) {
|
|
77
|
+
if (status === 'failed') {
|
|
78
|
+
process.stderr.write('Transcription failed.\n');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const msgCount = poll.message_count ?? poll.token_count ?? 0;
|
|
82
|
+
process.stderr.write(`Transcription complete: ${msgCount} ${flags.type === 'thread' ? 'messages' : 'tokens'}\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (!json)
|
|
88
|
+
process.stderr.write('Transcribing...\n');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Transient error — keep polling
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!json) {
|
|
96
|
+
if (data.thread_id) {
|
|
97
|
+
this.log(`\nImported as thread: ${data.thread_id}`);
|
|
98
|
+
this.log(`label: ${data.label ?? 'youtube-import'}`);
|
|
99
|
+
this.log(`\nReview the transcript:`);
|
|
100
|
+
this.log(` faces compile:thread:get ${data.thread_id}`);
|
|
101
|
+
this.log(`\nWhen ready to compile:`);
|
|
102
|
+
this.log(` faces compile:thread:make ${data.thread_id}`);
|
|
67
103
|
}
|
|
68
|
-
else {
|
|
69
|
-
|
|
70
|
-
this.log(`
|
|
71
|
-
this.log(
|
|
72
|
-
this.log(`
|
|
73
|
-
this.log(
|
|
74
|
-
this.log(`
|
|
75
|
-
this.log(` faces compile:thread:make ${thread.thread_id}`);
|
|
104
|
+
else if (data.document_id) {
|
|
105
|
+
this.log(`\nImported as document: ${data.document_id}`);
|
|
106
|
+
this.log(`label: ${data.label ?? 'youtube-import'}`);
|
|
107
|
+
this.log(`\nReview the document:`);
|
|
108
|
+
this.log(` faces compile:doc:get ${data.document_id}`);
|
|
109
|
+
this.log(`\nWhen ready to compile:`);
|
|
110
|
+
this.log(` faces compile:doc:make ${data.document_id}`);
|
|
76
111
|
}
|
|
77
112
|
}
|
|
78
113
|
return data;
|
|
@@ -2,7 +2,8 @@ import { BaseCommand } from '../../../base.js';
|
|
|
2
2
|
export default class CompileThreadEdit extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
|
-
label: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
|
+
label: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
'face-speaker': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -2,10 +2,13 @@ import { Args, Flags } from '@oclif/core';
|
|
|
2
2
|
import { BaseCommand } from '../../../base.js';
|
|
3
3
|
import { FacesAPIError } from '../../../client.js';
|
|
4
4
|
export default class CompileThreadEdit extends BaseCommand {
|
|
5
|
-
static description = 'Edit a thread (label)';
|
|
5
|
+
static description = 'Edit a thread (label and/or reassign face speaker)';
|
|
6
6
|
static flags = {
|
|
7
7
|
...BaseCommand.baseFlags,
|
|
8
|
-
label: Flags.string({ description: 'New thread label'
|
|
8
|
+
label: Flags.string({ description: 'New thread label' }),
|
|
9
|
+
'face-speaker': Flags.string({
|
|
10
|
+
description: 'Reassign face speaker: set this speaker to role=user, all others to role=assistant',
|
|
11
|
+
}),
|
|
9
12
|
};
|
|
10
13
|
static args = {
|
|
11
14
|
thread_id: Args.string({ description: 'Thread ID', required: true }),
|
|
@@ -13,16 +16,77 @@ export default class CompileThreadEdit extends BaseCommand {
|
|
|
13
16
|
async run() {
|
|
14
17
|
const { args, flags } = await this.parse(CompileThreadEdit);
|
|
15
18
|
const client = this.makeClient(flags);
|
|
19
|
+
const json = this.jsonEnabled();
|
|
20
|
+
if (!flags.label && !flags['face-speaker']) {
|
|
21
|
+
this.error('Provide at least one of --label or --face-speaker');
|
|
22
|
+
}
|
|
23
|
+
// If --face-speaker, fetch messages, remap roles, PATCH back
|
|
24
|
+
if (flags['face-speaker']) {
|
|
25
|
+
const speaker = flags['face-speaker'];
|
|
26
|
+
// Fetch current thread
|
|
27
|
+
let thread;
|
|
28
|
+
try {
|
|
29
|
+
thread = await client.get(`/v1/compile/threads/${args.thread_id}`);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err instanceof FacesAPIError)
|
|
33
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
const messages = thread.messages;
|
|
37
|
+
if (!messages || messages.length === 0) {
|
|
38
|
+
this.error('Thread has no messages to remap');
|
|
39
|
+
}
|
|
40
|
+
// Remap roles
|
|
41
|
+
let remapped = 0;
|
|
42
|
+
const updated = messages.map((m) => {
|
|
43
|
+
const isFace = m.name && m.name.toLowerCase() === speaker.toLowerCase();
|
|
44
|
+
const newRole = isFace ? 'user' : 'assistant';
|
|
45
|
+
if (m.role !== newRole)
|
|
46
|
+
remapped++;
|
|
47
|
+
return { role: newRole, content: m.content, from: m.name };
|
|
48
|
+
});
|
|
49
|
+
// PATCH messages
|
|
50
|
+
try {
|
|
51
|
+
await client.patch(`/v1/compile/threads/${args.thread_id}/messages`, {
|
|
52
|
+
body: { messages: updated },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err instanceof FacesAPIError)
|
|
57
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
if (!json) {
|
|
61
|
+
const userCount = updated.filter((m) => m.role === 'user').length;
|
|
62
|
+
const assistantCount = updated.filter((m) => m.role === 'assistant').length;
|
|
63
|
+
process.stderr.write(`Remapped ${remapped} messages: "${speaker}" → user (${userCount}), all others → assistant (${assistantCount})\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// If --label, update it
|
|
67
|
+
if (flags.label) {
|
|
68
|
+
try {
|
|
69
|
+
await client.patch(`/v1/compile/threads/${args.thread_id}`, { body: { label: flags.label } });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err instanceof FacesAPIError)
|
|
73
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
if (!json)
|
|
77
|
+
process.stderr.write(`Label updated to "${flags.label}"\n`);
|
|
78
|
+
}
|
|
79
|
+
// Fetch final state and return
|
|
16
80
|
let data;
|
|
17
81
|
try {
|
|
18
|
-
data = await client.
|
|
82
|
+
data = await client.get(`/v1/compile/threads/${args.thread_id}`);
|
|
19
83
|
}
|
|
20
84
|
catch (err) {
|
|
21
85
|
if (err instanceof FacesAPIError)
|
|
22
86
|
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
23
87
|
throw err;
|
|
24
88
|
}
|
|
25
|
-
if (!
|
|
89
|
+
if (!json)
|
|
26
90
|
this.printHuman(data);
|
|
27
91
|
return data;
|
|
28
92
|
}
|
|
@@ -3,6 +3,9 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { BaseCommand } from '../../base.js';
|
|
5
5
|
import { FacesAPIError } from '../../client.js';
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
6
9
|
export default class CompileUpload extends BaseCommand {
|
|
7
10
|
static description = 'Upload a file (text/PDF/audio/video) to compile into a face';
|
|
8
11
|
static flags = {
|
|
@@ -28,13 +31,13 @@ export default class CompileUpload extends BaseCommand {
|
|
|
28
31
|
async run() {
|
|
29
32
|
const { args, flags } = await this.parse(CompileUpload);
|
|
30
33
|
const client = this.makeClient(flags);
|
|
34
|
+
const json = this.jsonEnabled();
|
|
31
35
|
if (!fs.existsSync(flags.file))
|
|
32
36
|
this.error(`File not found: ${flags.file}`);
|
|
33
37
|
const filename = path.basename(flags.file);
|
|
34
38
|
const fileBlob = new Blob([fs.readFileSync(flags.file)], { type: 'application/octet-stream' });
|
|
35
39
|
const form = new FormData();
|
|
36
40
|
form.append('file', fileBlob, filename);
|
|
37
|
-
// type, perspective, face_speaker are query params (not form fields)
|
|
38
41
|
const params = new URLSearchParams();
|
|
39
42
|
params.set('type', flags.kind);
|
|
40
43
|
params.set('perspective', flags.perspective);
|
|
@@ -49,16 +52,55 @@ export default class CompileUpload extends BaseCommand {
|
|
|
49
52
|
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
50
53
|
throw err;
|
|
51
54
|
}
|
|
52
|
-
|
|
55
|
+
// If transcribing (audio/video upload), poll until complete
|
|
56
|
+
if (data.prepare_status === 'transcribing') {
|
|
57
|
+
const id = (data.thread_id ?? data.document_id);
|
|
58
|
+
const endpoint = flags.kind === 'thread'
|
|
59
|
+
? `/v1/compile/threads/${id}`
|
|
60
|
+
: `/v1/compile/documents/${id}`;
|
|
61
|
+
if (!json)
|
|
62
|
+
process.stderr.write('Transcribing...\n');
|
|
63
|
+
const timeout = 3600_000; // 1 hour
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
while (Date.now() - start < timeout) {
|
|
66
|
+
await sleep(5000);
|
|
67
|
+
try {
|
|
68
|
+
const poll = await client.get(endpoint);
|
|
69
|
+
const status = poll.prepare_status;
|
|
70
|
+
if (status !== 'transcribing') {
|
|
71
|
+
data = poll;
|
|
72
|
+
if (!json) {
|
|
73
|
+
if (status === 'failed') {
|
|
74
|
+
process.stderr.write('Transcription failed.\n');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const msgCount = poll.message_count ?? 0;
|
|
78
|
+
process.stderr.write(`Transcription complete: ${msgCount} messages\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (!json)
|
|
84
|
+
process.stderr.write('Transcribing...\n');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Transient error — keep polling
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!json) {
|
|
53
92
|
this.printHuman(data);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.log(
|
|
57
|
-
this.log(
|
|
93
|
+
if (flags.kind === 'thread' && data.thread_id) {
|
|
94
|
+
this.log(`\nReview the transcript:`);
|
|
95
|
+
this.log(` faces compile:thread:get ${data.thread_id}`);
|
|
96
|
+
this.log(`\nWhen ready to compile:`);
|
|
97
|
+
this.log(` faces compile:thread:make ${data.thread_id}`);
|
|
58
98
|
}
|
|
59
|
-
else if (flags.kind === 'document' &&
|
|
60
|
-
this.log(`\
|
|
61
|
-
this.log(` faces compile:doc:
|
|
99
|
+
else if (flags.kind === 'document' && data.document_id) {
|
|
100
|
+
this.log(`\nReview the document:`);
|
|
101
|
+
this.log(` faces compile:doc:get ${data.document_id}`);
|
|
102
|
+
this.log(`\nWhen ready to compile:`);
|
|
103
|
+
this.log(` faces compile:doc:make ${data.document_id}`);
|
|
62
104
|
}
|
|
63
105
|
}
|
|
64
106
|
return data;
|