clipugc 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.
Files changed (77) hide show
  1. package/README.md +249 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +31 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/account.d.ts +12 -0
  6. package/dist/commands/account.js +48 -0
  7. package/dist/commands/account.js.map +1 -0
  8. package/dist/commands/auth.d.ts +14 -0
  9. package/dist/commands/auth.js +152 -0
  10. package/dist/commands/auth.js.map +1 -0
  11. package/dist/commands/characters.d.ts +3 -0
  12. package/dist/commands/characters.js +261 -0
  13. package/dist/commands/characters.js.map +1 -0
  14. package/dist/commands/config.d.ts +5 -0
  15. package/dist/commands/config.js +69 -0
  16. package/dist/commands/config.js.map +1 -0
  17. package/dist/commands/credits.d.ts +2 -0
  18. package/dist/commands/credits.js +117 -0
  19. package/dist/commands/credits.js.map +1 -0
  20. package/dist/commands/hooks.d.ts +2 -0
  21. package/dist/commands/hooks.js +32 -0
  22. package/dist/commands/hooks.js.map +1 -0
  23. package/dist/commands/images.d.ts +3 -0
  24. package/dist/commands/images.js +257 -0
  25. package/dist/commands/images.js.map +1 -0
  26. package/dist/commands/videos.d.ts +2 -0
  27. package/dist/commands/videos.js +272 -0
  28. package/dist/commands/videos.js.map +1 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +24 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/services/api.d.ts +40 -0
  33. package/dist/services/api.js +139 -0
  34. package/dist/services/api.js.map +1 -0
  35. package/dist/services/characters.service.d.ts +135 -0
  36. package/dist/services/characters.service.js +276 -0
  37. package/dist/services/characters.service.js.map +1 -0
  38. package/dist/services/hooks.service.d.ts +9 -0
  39. package/dist/services/hooks.service.js +23 -0
  40. package/dist/services/hooks.service.js.map +1 -0
  41. package/dist/services/images.service.d.ts +95 -0
  42. package/dist/services/images.service.js +191 -0
  43. package/dist/services/images.service.js.map +1 -0
  44. package/dist/services/upload.service.d.ts +46 -0
  45. package/dist/services/upload.service.js +134 -0
  46. package/dist/services/upload.service.js.map +1 -0
  47. package/dist/services/user.service.d.ts +80 -0
  48. package/dist/services/user.service.js +35 -0
  49. package/dist/services/user.service.js.map +1 -0
  50. package/dist/services/videos.service.d.ts +100 -0
  51. package/dist/services/videos.service.js +187 -0
  52. package/dist/services/videos.service.js.map +1 -0
  53. package/dist/types/index.d.ts +47 -0
  54. package/dist/types/index.js +18 -0
  55. package/dist/types/index.js.map +1 -0
  56. package/dist/utils/config.d.ts +18 -0
  57. package/dist/utils/config.js +65 -0
  58. package/dist/utils/config.js.map +1 -0
  59. package/dist/utils/errors.d.ts +52 -0
  60. package/dist/utils/errors.js +74 -0
  61. package/dist/utils/errors.js.map +1 -0
  62. package/dist/utils/logger.d.ts +11 -0
  63. package/dist/utils/logger.js +28 -0
  64. package/dist/utils/logger.js.map +1 -0
  65. package/dist/utils/output.d.ts +16 -0
  66. package/dist/utils/output.js +53 -0
  67. package/dist/utils/output.js.map +1 -0
  68. package/dist/utils/poll.d.ts +37 -0
  69. package/dist/utils/poll.js +94 -0
  70. package/dist/utils/poll.js.map +1 -0
  71. package/dist/utils/prompt.d.ts +4 -0
  72. package/dist/utils/prompt.js +55 -0
  73. package/dist/utils/prompt.js.map +1 -0
  74. package/dist/version.d.ts +1 -0
  75. package/dist/version.js +12 -0
  76. package/dist/version.js.map +1 -0
  77. package/package.json +49 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Character images (looks) service — request building, client-side validation
3
+ * and API calls for the `clipugc images` command group.
4
+ *
5
+ * All validation lives here (unit-testable); command handlers stay thin.
6
+ * Throws typed errors from utils/errors.js — never exits the process.
7
+ */
8
+ /** A character look (reference image) record. Unknown fields are preserved. */
9
+ export interface CharacterImage {
10
+ id: number | string;
11
+ shot_type?: string;
12
+ status?: string;
13
+ image_url?: string;
14
+ url?: string;
15
+ created_at?: string;
16
+ [key: string]: unknown;
17
+ }
18
+ /** Payload of GET /character-images/{id}/check-status. */
19
+ export interface ImageStatusCheck {
20
+ status: string;
21
+ failure_reason?: string | null;
22
+ error_message?: string | null;
23
+ [key: string]: unknown;
24
+ }
25
+ export declare const SHOT_TYPES: readonly ["frontal", "three_quarter", "profile", "back"];
26
+ export declare const TEMPLATES: readonly ["model_digitals", "scene_recreation", "specific_angle"];
27
+ export declare const RESOLUTIONS: readonly ["0.5K", "1K", "2K", "4K"];
28
+ export declare const DEFAULT_SHOTS: string[];
29
+ export declare const DEFAULT_TEMPLATE = "model_digitals";
30
+ export declare const DEFAULT_RESOLUTION = "2K";
31
+ export declare const SCENE_MAX = 600;
32
+ export declare const VARIATION_SCENE_MIN = 3;
33
+ export declare const VARIATION_COUNT_MIN = 1;
34
+ export declare const VARIATION_COUNT_MAX = 4;
35
+ /** Keys the API might wrap an image list in (tolerant unwrapping). */
36
+ export declare const IMAGE_LIST_KEYS: string[];
37
+ /**
38
+ * Parse a comma-separated --shots value into a validated shot_types array.
39
+ * Defaults to ['frontal'] when omitted/empty.
40
+ */
41
+ export declare function parseShotTypes(input?: string): string[];
42
+ export interface GenerateImagesInput {
43
+ /** Comma-separated shot types, e.g. "frontal,three_quarter". */
44
+ shots?: string;
45
+ template?: string;
46
+ scene?: string;
47
+ resolution?: string;
48
+ }
49
+ /** Build + validate the POST /ai-characters/{id}/reference-images payload. */
50
+ export declare function buildGeneratePayload(input?: GenerateImagesInput): Record<string, unknown>;
51
+ /**
52
+ * Normalize the create/variation response into an array of image records:
53
+ * handles a single object with id, a bare array, or a wrapped array.
54
+ */
55
+ export declare function extractCreatedImages(data: unknown): CharacterImage[];
56
+ export interface GenerateImagesResult {
57
+ /** Raw response data (for --json passthrough). */
58
+ raw: unknown;
59
+ /** Normalized created image record(s). */
60
+ images: CharacterImage[];
61
+ }
62
+ /** POST /ai-characters/{id}/reference-images */
63
+ export declare function generateImages(characterId: string, input?: GenerateImagesInput): Promise<GenerateImagesResult>;
64
+ /** GET /ai-characters/{id}/reference-images */
65
+ export declare function listImages(characterId: string): Promise<{
66
+ items: CharacterImage[];
67
+ raw: unknown;
68
+ }>;
69
+ /** GET /character-images/{id} */
70
+ export declare function getImage(id: string): Promise<CharacterImage>;
71
+ /** GET /character-images/{id}/check-status — returned as-is (poll payload). */
72
+ export declare function checkImageStatus(id: string): Promise<ImageStatusCheck>;
73
+ /**
74
+ * Download a look's image to disk. The API serves signed temporary URLs on the
75
+ * image record (media.image_url), so we fetch the record first, then the bytes.
76
+ */
77
+ export declare function downloadImage(id: string, opts?: {
78
+ output?: string;
79
+ quiet?: boolean;
80
+ }): Promise<string>;
81
+ export interface VariationInput {
82
+ scene: string;
83
+ count?: number;
84
+ beforeAfter?: boolean;
85
+ }
86
+ /** Build + validate the POST /character-images/{id}/variation payload. */
87
+ export declare function buildVariationPayload(input: VariationInput): Record<string, unknown>;
88
+ /** POST /character-images/{id}/variation */
89
+ export declare function createVariation(id: string, input: VariationInput): Promise<GenerateImagesResult>;
90
+ /** POST /character-images/{id}/retry */
91
+ export declare function retryImage(id: string): Promise<unknown>;
92
+ /** DELETE /character-images/{id} */
93
+ export declare function deleteImage(id: string): Promise<unknown>;
94
+ /** Best-effort URL lookup on an image/status record (field name varies). */
95
+ export declare function findImageUrl(record: Record<string, unknown>): string | undefined;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Character images (looks) service — request building, client-side validation
3
+ * and API calls for the `clipugc images` command group.
4
+ *
5
+ * All validation lives here (unit-testable); command handlers stay thin.
6
+ * Throws typed errors from utils/errors.js — never exits the process.
7
+ */
8
+ import { createApiClient } from './api.js';
9
+ import { ValidationError } from '../utils/errors.js';
10
+ import { extractList } from './characters.service.js';
11
+ export const SHOT_TYPES = ['frontal', 'three_quarter', 'profile', 'back'];
12
+ export const TEMPLATES = ['model_digitals', 'scene_recreation', 'specific_angle'];
13
+ export const RESOLUTIONS = ['0.5K', '1K', '2K', '4K'];
14
+ export const DEFAULT_SHOTS = ['frontal'];
15
+ export const DEFAULT_TEMPLATE = 'model_digitals';
16
+ export const DEFAULT_RESOLUTION = '2K';
17
+ export const SCENE_MAX = 600;
18
+ export const VARIATION_SCENE_MIN = 3;
19
+ export const VARIATION_COUNT_MIN = 1;
20
+ export const VARIATION_COUNT_MAX = 4;
21
+ /** Keys the API might wrap an image list in (tolerant unwrapping). */
22
+ export const IMAGE_LIST_KEYS = ['items', 'images', 'reference_images', 'character_images', 'data'];
23
+ /**
24
+ * Parse a comma-separated --shots value into a validated shot_types array.
25
+ * Defaults to ['frontal'] when omitted/empty.
26
+ */
27
+ export function parseShotTypes(input) {
28
+ if (input === undefined || input.trim() === '')
29
+ return [...DEFAULT_SHOTS];
30
+ const shots = input
31
+ .split(',')
32
+ .map((s) => s.trim())
33
+ .filter((s) => s !== '');
34
+ if (shots.length === 0)
35
+ return [...DEFAULT_SHOTS];
36
+ for (const shot of shots) {
37
+ if (!SHOT_TYPES.includes(shot)) {
38
+ throw new ValidationError(`Invalid shot type "${shot}". Allowed: ${SHOT_TYPES.join(', ')}.`);
39
+ }
40
+ }
41
+ return shots;
42
+ }
43
+ /** Build + validate the POST /ai-characters/{id}/reference-images payload. */
44
+ export function buildGeneratePayload(input = {}) {
45
+ const payload = {
46
+ shot_types: parseShotTypes(input.shots),
47
+ };
48
+ // Template is optional server-side (auto: scene → scene_recreation, else model_digitals).
49
+ if (input.template !== undefined && input.template !== '') {
50
+ if (!TEMPLATES.includes(input.template)) {
51
+ throw new ValidationError(`Invalid template "${input.template}". Allowed: ${TEMPLATES.join(', ')}.`);
52
+ }
53
+ payload.template = input.template;
54
+ }
55
+ if (input.scene !== undefined && input.scene !== '') {
56
+ if (input.scene.length > SCENE_MAX) {
57
+ throw new ValidationError(`Scene prompt must be at most ${SCENE_MAX} characters (got ${input.scene.length}).`);
58
+ }
59
+ payload.scene_prompt = input.scene;
60
+ }
61
+ const resolution = input.resolution === undefined || input.resolution === '' ? DEFAULT_RESOLUTION : input.resolution;
62
+ if (!RESOLUTIONS.includes(resolution)) {
63
+ throw new ValidationError(`Invalid resolution "${resolution}". Allowed: ${RESOLUTIONS.join(', ')}.`);
64
+ }
65
+ payload.resolution = resolution;
66
+ return payload;
67
+ }
68
+ /**
69
+ * Normalize the create/variation response into an array of image records:
70
+ * handles a single object with id, a bare array, or a wrapped array.
71
+ */
72
+ export function extractCreatedImages(data) {
73
+ if (Array.isArray(data))
74
+ return data.filter((x) => Boolean(x) && typeof x === 'object');
75
+ if (data && typeof data === 'object') {
76
+ const obj = data;
77
+ if (obj.id !== undefined)
78
+ return [obj];
79
+ const wrapped = extractList(data, IMAGE_LIST_KEYS);
80
+ if (wrapped.length > 0)
81
+ return wrapped;
82
+ }
83
+ return [];
84
+ }
85
+ /** POST /ai-characters/{id}/reference-images */
86
+ export async function generateImages(characterId, input = {}) {
87
+ const payload = buildGeneratePayload(input);
88
+ const api = await createApiClient();
89
+ const raw = await api.post(`/ai-characters/${characterId}/reference-images`, { body: payload });
90
+ return { raw, images: extractCreatedImages(raw) };
91
+ }
92
+ /** GET /ai-characters/{id}/reference-images */
93
+ export async function listImages(characterId) {
94
+ const api = await createApiClient();
95
+ const raw = await api.get(`/ai-characters/${characterId}/reference-images`);
96
+ return { items: extractList(raw, IMAGE_LIST_KEYS), raw };
97
+ }
98
+ /** GET /character-images/{id} */
99
+ export async function getImage(id) {
100
+ const api = await createApiClient();
101
+ const data = await api.get(`/character-images/${id}`);
102
+ const images = extractCreatedImages(data);
103
+ return images[0] ?? data;
104
+ }
105
+ /** GET /character-images/{id}/check-status — returned as-is (poll payload). */
106
+ export async function checkImageStatus(id) {
107
+ const api = await createApiClient();
108
+ return api.get(`/character-images/${id}/check-status`);
109
+ }
110
+ /**
111
+ * Download a look's image to disk. The API serves signed temporary URLs on the
112
+ * image record (media.image_url), so we fetch the record first, then the bytes.
113
+ */
114
+ export async function downloadImage(id, opts = {}) {
115
+ const image = await getImage(id);
116
+ const url = findImageUrl(image);
117
+ if (!url) {
118
+ throw new ValidationError(`Look ${id} has no image yet — check \`clipugc images status ${id}\`.`);
119
+ }
120
+ const extension = /\.(png|jpe?g|webp)(\?|$)/i.exec(url)?.[1]?.toLowerCase() ?? 'png';
121
+ const dest = opts.output || `clipugc-look-${id}.${extension}`;
122
+ const { NetworkError } = await import('../utils/errors.js');
123
+ let response;
124
+ try {
125
+ response = await fetch(url);
126
+ }
127
+ catch (err) {
128
+ const reason = err instanceof Error ? err.message : String(err);
129
+ throw new NetworkError(`Download failed: ${reason}`);
130
+ }
131
+ if (!response.ok) {
132
+ throw new NetworkError(`Download failed (HTTP ${response.status}). The link may have expired — try again.`);
133
+ }
134
+ const fs = (await import('fs-extra')).default;
135
+ // outputFile creates missing parent directories, so -o can target
136
+ // workspace paths like clipugc/influencers/<id>/pictures/… directly.
137
+ await fs.outputFile(dest, Buffer.from(await response.arrayBuffer()));
138
+ return dest;
139
+ }
140
+ /** Build + validate the POST /character-images/{id}/variation payload. */
141
+ export function buildVariationPayload(input) {
142
+ const scene = (input.scene ?? '').trim();
143
+ if (scene.length < VARIATION_SCENE_MIN || scene.length > SCENE_MAX) {
144
+ throw new ValidationError(`Variation scene prompt must be ${VARIATION_SCENE_MIN}-${SCENE_MAX} characters (got ${scene.length}).`);
145
+ }
146
+ const payload = { scene_prompt: scene };
147
+ if (input.count !== undefined) {
148
+ if (!Number.isInteger(input.count) || input.count < VARIATION_COUNT_MIN || input.count > VARIATION_COUNT_MAX) {
149
+ throw new ValidationError(`--count must be an integer between ${VARIATION_COUNT_MIN} and ${VARIATION_COUNT_MAX} (got ${input.count}).`);
150
+ }
151
+ payload.count = input.count;
152
+ }
153
+ if (input.beforeAfter !== undefined)
154
+ payload.before_after = input.beforeAfter;
155
+ return payload;
156
+ }
157
+ /** POST /character-images/{id}/variation */
158
+ export async function createVariation(id, input) {
159
+ const payload = buildVariationPayload(input);
160
+ const api = await createApiClient();
161
+ const raw = await api.post(`/character-images/${id}/variation`, { body: payload });
162
+ return { raw, images: extractCreatedImages(raw) };
163
+ }
164
+ /** POST /character-images/{id}/retry */
165
+ export async function retryImage(id) {
166
+ const api = await createApiClient();
167
+ return api.post(`/character-images/${id}/retry`);
168
+ }
169
+ /** DELETE /character-images/{id} */
170
+ export async function deleteImage(id) {
171
+ const api = await createApiClient();
172
+ return api.delete(`/character-images/${id}`);
173
+ }
174
+ /** Best-effort URL lookup on an image/status record (field name varies). */
175
+ export function findImageUrl(record) {
176
+ const candidates = [record.image_url, record.url, record.file_url, record.public_url];
177
+ for (const c of candidates) {
178
+ if (typeof c === 'string' && c !== '')
179
+ return c;
180
+ }
181
+ for (const key of ['media', 'image']) {
182
+ const nested = record[key];
183
+ if (nested && typeof nested === 'object') {
184
+ const url = nested.image_url ?? nested.url;
185
+ if (typeof url === 'string' && url !== '')
186
+ return url;
187
+ }
188
+ }
189
+ return undefined;
190
+ }
191
+ //# sourceMappingURL=images.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"images.service.js","sourceRoot":"","sources":["../../src/services/images.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { ApiClient } from './api.js';
2
+ /** What the uploaded file will be used for — determines allowed extensions. */
3
+ export type UploadPurpose = 'driver_video' | 'app_video' | 'music' | 'photo';
4
+ /** Allowed file extensions (lowercase, no dot) per upload purpose. */
5
+ export declare const ALLOWED_EXTENSIONS: Record<UploadPurpose, readonly string[]>;
6
+ /** Driver videos must stay small enough for motion-control processing. */
7
+ export declare const MAX_DRIVER_VIDEO_BYTES: number;
8
+ export declare const MAX_DRIVER_VIDEO_SECONDS = 30;
9
+ /** Response of POST /uploads/presign. */
10
+ export interface PresignedUpload {
11
+ key: string;
12
+ upload_url: string;
13
+ method: string;
14
+ headers?: Record<string, string>;
15
+ expires_at?: string;
16
+ [key: string]: unknown;
17
+ }
18
+ /**
19
+ * Pure validation: the file's extension must be allowed for the purpose
20
+ * (case-insensitive). Returns the lowercase extension without the dot.
21
+ */
22
+ export declare function validateExtension(purpose: UploadPurpose, filePath: string): string;
23
+ /** Pure validation: driver video file size must be at most 50 MB. */
24
+ export declare function validateDriverVideoSize(sizeBytes: number): void;
25
+ /** Pure validation: driver video duration must be at most 30 seconds. */
26
+ export declare function validateDriverVideoDuration(durationSeconds: number): void;
27
+ /**
28
+ * Probe a media file's duration in seconds via ffprobe.
29
+ * Returns null when ffprobe is not installed or fails — callers skip the
30
+ * duration check in that case (the server enforces it anyway).
31
+ */
32
+ export declare function probeDurationSeconds(filePath: string): Promise<number | null>;
33
+ export interface UploadOptions {
34
+ /** Suppress the spinner (used in --json mode); the upload still runs. */
35
+ quiet?: boolean;
36
+ /** Duration probe override (tests inject this to avoid invoking ffprobe). */
37
+ probe?: (filePath: string) => Promise<number | null>;
38
+ }
39
+ /**
40
+ * Upload a local file for the given purpose:
41
+ * 1. validate existence + extension (+ size/duration for driver videos),
42
+ * 2. POST /uploads/presign to get a direct-to-storage URL,
43
+ * 3. PUT the raw bytes to that URL (not the API envelope),
44
+ * and return the storage `key` to reference in follow-up API calls.
45
+ */
46
+ export declare function uploadFile(api: ApiClient, purpose: UploadPurpose, filePath: string, opts?: UploadOptions): Promise<string>;
@@ -0,0 +1,134 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import ora from 'ora';
6
+ import { NetworkError, ValidationError } from '../utils/errors.js';
7
+ const execFileAsync = promisify(execFile);
8
+ /** Allowed file extensions (lowercase, no dot) per upload purpose. */
9
+ export const ALLOWED_EXTENSIONS = {
10
+ driver_video: ['mp4', 'mov'],
11
+ app_video: ['mp4', 'mov'],
12
+ music: ['mp3', 'wav', 'm4a'],
13
+ photo: ['png', 'jpg', 'jpeg', 'webp'],
14
+ };
15
+ /** Driver videos must stay small enough for motion-control processing. */
16
+ export const MAX_DRIVER_VIDEO_BYTES = 50 * 1024 * 1024; // 50 MB
17
+ export const MAX_DRIVER_VIDEO_SECONDS = 30;
18
+ /**
19
+ * Pure validation: the file's extension must be allowed for the purpose
20
+ * (case-insensitive). Returns the lowercase extension without the dot.
21
+ */
22
+ export function validateExtension(purpose, filePath) {
23
+ const ext = path.extname(filePath).replace(/^\./, '').toLowerCase();
24
+ const allowed = ALLOWED_EXTENSIONS[purpose];
25
+ if (!ext || !allowed.includes(ext)) {
26
+ throw new ValidationError(`Unsupported file type ".${ext || '?'}" for ${purpose.replace('_', ' ')} uploads. ` +
27
+ `Allowed: ${allowed.map((e) => `.${e}`).join(', ')}.`);
28
+ }
29
+ return ext;
30
+ }
31
+ /** Pure validation: driver video file size must be at most 50 MB. */
32
+ export function validateDriverVideoSize(sizeBytes) {
33
+ if (sizeBytes > MAX_DRIVER_VIDEO_BYTES) {
34
+ const mb = (sizeBytes / (1024 * 1024)).toFixed(1);
35
+ throw new ValidationError(`Driver video is ${mb} MB, but the maximum is 50 MB. Compress or trim the video and try again.`);
36
+ }
37
+ }
38
+ /** Pure validation: driver video duration must be at most 30 seconds. */
39
+ export function validateDriverVideoDuration(durationSeconds) {
40
+ if (durationSeconds > MAX_DRIVER_VIDEO_SECONDS) {
41
+ throw new ValidationError(`Driver video is ${Math.round(durationSeconds)}s long, but the maximum is ${MAX_DRIVER_VIDEO_SECONDS}s. ` +
42
+ 'Trim the video (e.g. with ffmpeg or any editor) and try again.');
43
+ }
44
+ }
45
+ /**
46
+ * Probe a media file's duration in seconds via ffprobe.
47
+ * Returns null when ffprobe is not installed or fails — callers skip the
48
+ * duration check in that case (the server enforces it anyway).
49
+ */
50
+ export async function probeDurationSeconds(filePath) {
51
+ try {
52
+ const { stdout } = await execFileAsync('ffprobe', [
53
+ '-v',
54
+ 'error',
55
+ '-show_entries',
56
+ 'format=duration',
57
+ '-of',
58
+ 'csv=p=0',
59
+ filePath,
60
+ ]);
61
+ const duration = Number.parseFloat(stdout.trim());
62
+ return Number.isFinite(duration) ? duration : null;
63
+ }
64
+ catch {
65
+ return null; // ffprobe missing or unreadable file — skip silently
66
+ }
67
+ }
68
+ /**
69
+ * Upload a local file for the given purpose:
70
+ * 1. validate existence + extension (+ size/duration for driver videos),
71
+ * 2. POST /uploads/presign to get a direct-to-storage URL,
72
+ * 3. PUT the raw bytes to that URL (not the API envelope),
73
+ * and return the storage `key` to reference in follow-up API calls.
74
+ */
75
+ export async function uploadFile(api, purpose, filePath, opts = {}) {
76
+ const extension = validateExtension(purpose, filePath);
77
+ let stat;
78
+ try {
79
+ stat = await fs.stat(filePath);
80
+ }
81
+ catch {
82
+ throw new ValidationError(`File not found: ${filePath}`);
83
+ }
84
+ if (!stat.isFile()) {
85
+ throw new ValidationError(`Not a file: ${filePath}`);
86
+ }
87
+ if (purpose === 'driver_video') {
88
+ validateDriverVideoSize(stat.size);
89
+ const probe = opts.probe ?? probeDurationSeconds;
90
+ const duration = await probe(filePath);
91
+ if (duration !== null) {
92
+ validateDriverVideoDuration(duration);
93
+ }
94
+ }
95
+ const basename = path.basename(filePath);
96
+ const sizeMb = (stat.size / (1024 * 1024)).toFixed(1);
97
+ const spinner = opts.quiet ? null : ora(`Uploading ${basename} (${sizeMb} MB)`).start();
98
+ try {
99
+ const presign = await api.post('/uploads/presign', {
100
+ body: { purpose, extension },
101
+ });
102
+ const body = await fs.readFile(filePath);
103
+ let response;
104
+ try {
105
+ response = await fetch(presign.upload_url, {
106
+ method: presign.method || 'PUT',
107
+ headers: presign.headers ?? {},
108
+ body,
109
+ });
110
+ }
111
+ catch (err) {
112
+ const reason = err instanceof Error ? err.message : String(err);
113
+ throw new NetworkError(`Upload of ${basename} failed: ${reason}`);
114
+ }
115
+ if (!response.ok) {
116
+ let snippet = '';
117
+ try {
118
+ snippet = (await response.text()).slice(0, 200);
119
+ }
120
+ catch {
121
+ // ignore unreadable body
122
+ }
123
+ throw new NetworkError(`Upload of ${basename} failed (HTTP ${response.status})${snippet ? `: ${snippet}` : ''}`);
124
+ }
125
+ spinner?.succeed(`Uploaded ${basename}`);
126
+ return presign.key;
127
+ }
128
+ catch (err) {
129
+ if (spinner?.isSpinning)
130
+ spinner.fail(`Upload of ${basename} failed`);
131
+ throw err;
132
+ }
133
+ }
134
+ //# sourceMappingURL=upload.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.service.js","sourceRoot":"","sources":["../../src/services/upload.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import type { ApiClient } from './api.js';
2
+ import type { Pagination } from '../types/index.js';
3
+ /**
4
+ * User / account / credits service (G1 group).
5
+ * All functions throw typed errors from src/utils/errors.ts via ApiClient;
6
+ * they never exit the process.
7
+ */
8
+ /**
9
+ * Authenticated user profile returned by GET /user.
10
+ * The API may add fields over time, so unknown fields are kept via the
11
+ * index signature and printed generically by `whoami`.
12
+ */
13
+ export interface UserProfile {
14
+ id?: number | string;
15
+ name?: string | null;
16
+ email?: string | null;
17
+ [key: string]: unknown;
18
+ }
19
+ /**
20
+ * Per-action credit costs returned by GET /credits.
21
+ * The server charges duration-aware, so several video costs coexist:
22
+ * image a reference-image generation
23
+ * clip a 5s image-to-video clip
24
+ * clip_10s a 10s image-to-video clip
25
+ * motion_per_second motion-control charge per second of driver video
26
+ * (× ceil(driver seconds), capped at 30s)
27
+ * scene_staged a scene-staged clip (created with a --scene prompt)
28
+ * merge merging a clip with an app recording into the final ad
29
+ * Unknown keys are tolerated via the index signature so new server-side
30
+ * actions render generically.
31
+ */
32
+ export interface CreditCosts {
33
+ image?: number;
34
+ clip?: number;
35
+ clip_10s?: number;
36
+ motion_per_second?: number;
37
+ scene_staged?: number;
38
+ merge?: number;
39
+ [action: string]: number | undefined;
40
+ }
41
+ /** Response of GET /credits. */
42
+ export interface CreditsInfo {
43
+ /** Current credit balance. */
44
+ balance: number;
45
+ /** Per-action credit costs, e.g. { image: 2, clip: 7, clip_10s: 13, merge: 1 }. */
46
+ costs: CreditCosts;
47
+ }
48
+ /** A single credit ledger entry returned by GET /credits/transactions. */
49
+ export interface CreditTransaction {
50
+ id?: number | string;
51
+ uuid?: string;
52
+ type?: string;
53
+ action?: string;
54
+ /** Signed amount — negative means a spend, positive means a top-up/refund. */
55
+ amount?: number;
56
+ balance_after?: number;
57
+ description?: string | null;
58
+ created_at?: string;
59
+ [key: string]: unknown;
60
+ }
61
+ /** Payload of GET /credits/transactions. */
62
+ export interface CreditTransactionsResult {
63
+ transactions: CreditTransaction[];
64
+ pagination?: Pagination;
65
+ }
66
+ /** Fetch the authenticated user's profile (GET /user). */
67
+ export declare function getUser(api: ApiClient): Promise<UserProfile>;
68
+ /** Fetch the credit balance and per-action costs (GET /credits). */
69
+ export declare function getCredits(api: ApiClient): Promise<CreditsInfo>;
70
+ /**
71
+ * Fetch the credit transaction history (GET /credits/transactions).
72
+ * `perPage` maps to the `per_page` query param; `page` to `page`.
73
+ * Tolerant to the API returning the array raw or wrapped under `transactions`.
74
+ */
75
+ export declare function getCreditTransactions(api: ApiClient, opts?: {
76
+ perPage?: number;
77
+ page?: number;
78
+ }): Promise<CreditTransactionsResult>;
79
+ /** Permanently delete the authenticated user's account (DELETE /user). */
80
+ export declare function deleteAccount(api: ApiClient): Promise<void>;
@@ -0,0 +1,35 @@
1
+ /** Fetch the authenticated user's profile (GET /user). */
2
+ export async function getUser(api) {
3
+ return api.get('/user');
4
+ }
5
+ /** Fetch the credit balance and per-action costs (GET /credits). */
6
+ export async function getCredits(api) {
7
+ return api.get('/credits');
8
+ }
9
+ /**
10
+ * Fetch the credit transaction history (GET /credits/transactions).
11
+ * `perPage` maps to the `per_page` query param; `page` to `page`.
12
+ * Tolerant to the API returning the array raw or wrapped under `transactions`.
13
+ */
14
+ export async function getCreditTransactions(api, opts = {}) {
15
+ const raw = await api.get('/credits/transactions', {
16
+ query: { per_page: opts.perPage, page: opts.page },
17
+ });
18
+ if (Array.isArray(raw)) {
19
+ return { transactions: raw };
20
+ }
21
+ if (raw && typeof raw === 'object') {
22
+ const record = raw;
23
+ const transactions = Array.isArray(record.transactions)
24
+ ? record.transactions
25
+ : [];
26
+ const pagination = record.pagination ?? undefined;
27
+ return { transactions, pagination };
28
+ }
29
+ return { transactions: [] };
30
+ }
31
+ /** Permanently delete the authenticated user's account (DELETE /user). */
32
+ export async function deleteAccount(api) {
33
+ await api.delete('/user');
34
+ }
35
+ //# sourceMappingURL=user.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user.service.js","sourceRoot":"","sources":["../../src/services/user.service.ts"],"names":[],"mappings":"AAwEA,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,GAAc;IAC1C,OAAO,GAAG,CAAC,GAAG,CAAc,OAAO,CAAC,CAAC;AACvC,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAc;IAC7C,OAAO,GAAG,CAAC,GAAG,CAAc,UAAU,CAAC,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,GAAc,EACd,OAA4C,EAAE;IAE9C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,CAAU,uBAAuB,EAAE;QAC1D,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;KACnD,CAAC,CAAC;IACH,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,YAAY,EAAE,GAA0B,EAAE,CAAC;IACtD,CAAC;IACD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,GAA8B,CAAC;QAC9C,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC;YACrD,CAAC,CAAE,MAAM,CAAC,YAAoC;YAC9C,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,UAAU,GAAI,MAAM,CAAC,UAAqC,IAAI,SAAS,CAAC;QAC9E,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;AAC9B,CAAC;AAED,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAc;IAChD,MAAM,GAAG,CAAC,MAAM,CAAU,OAAO,CAAC,CAAC;AACrC,CAAC"}