faces-cli 1.4.5 → 1.5.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.
@@ -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
- this.log('Importing… (transcription may take 10–60s depending on video length)');
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
- if (!this.jsonEnabled()) {
57
- const res = data;
58
- if (res.type === 'document') {
59
- const doc = res;
60
- this.log(`Imported as document`);
61
- this.log(`id: ${doc.document_id}`);
62
- this.log(`label: ${doc.label}`);
63
- this.log(`tokens: ${doc.token_count}`);
64
- this.log(``);
65
- this.log(`Next step:`);
66
- this.log(` faces compile:doc:make ${doc.document_id}`);
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
- const thread = res;
70
- this.log(`Imported as thread`);
71
- this.log(`id: ${thread.thread_id}`);
72
- this.log(`label: ${thread.label}`);
73
- this.log(``);
74
- this.log(`Next step:`);
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;
@@ -0,0 +1,17 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class CompileUpload extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
+ kind: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ perspective: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'face-speaker': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ static args: {
14
+ alias: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
15
+ };
16
+ run(): Promise<unknown>;
17
+ }
@@ -0,0 +1,108 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { BaseCommand } from '../../base.js';
5
+ import { FacesAPIError } from '../../client.js';
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ export default class CompileUpload extends BaseCommand {
10
+ static description = 'Upload a file (text/PDF/audio/video) to compile into a face';
11
+ static flags = {
12
+ ...BaseCommand.baseFlags,
13
+ file: Flags.string({ description: 'File to upload', required: true }),
14
+ kind: Flags.string({
15
+ description: 'Upload kind: document or thread',
16
+ options: ['document', 'thread'],
17
+ default: 'document',
18
+ }),
19
+ perspective: Flags.string({
20
+ description: 'Perspective (document only)',
21
+ options: ['first-person', 'third-person'],
22
+ default: 'third-person',
23
+ }),
24
+ 'face-speaker': Flags.string({
25
+ description: 'Speaker name to map to the face (thread only). If omitted, auto-detected from face name.',
26
+ }),
27
+ };
28
+ static args = {
29
+ alias: Args.string({ description: 'Face alias', required: true }),
30
+ };
31
+ async run() {
32
+ const { args, flags } = await this.parse(CompileUpload);
33
+ const client = this.makeClient(flags);
34
+ const json = this.jsonEnabled();
35
+ if (!fs.existsSync(flags.file))
36
+ this.error(`File not found: ${flags.file}`);
37
+ const filename = path.basename(flags.file);
38
+ const fileBlob = new Blob([fs.readFileSync(flags.file)], { type: 'application/octet-stream' });
39
+ const form = new FormData();
40
+ form.append('file', fileBlob, filename);
41
+ const params = new URLSearchParams();
42
+ params.set('type', flags.kind);
43
+ params.set('perspective', flags.perspective);
44
+ if (flags['face-speaker'])
45
+ params.set('face_speaker', flags['face-speaker']);
46
+ let data;
47
+ try {
48
+ data = await client.postForm(`/v1/faces/${args.alias}/upload?${params}`, form);
49
+ }
50
+ catch (err) {
51
+ if (err instanceof FacesAPIError)
52
+ this.error(`Error (${err.statusCode}): ${err.message}`);
53
+ throw err;
54
+ }
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) {
92
+ this.printHuman(data);
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}`);
98
+ }
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}`);
104
+ }
105
+ }
106
+ return data;
107
+ }
108
+ }