@gannochenko/staticstripes 0.0.12 → 0.0.15
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/Makefile +20 -0
- package/dist/asset-manager.d.ts +1 -0
- package/dist/asset-manager.d.ts.map +1 -1
- package/dist/asset-manager.js +3 -0
- package/dist/asset-manager.js.map +1 -1
- package/dist/cli/ai-generation-strategy-factory.d.ts +23 -0
- package/dist/cli/ai-generation-strategy-factory.d.ts.map +1 -0
- package/dist/cli/ai-generation-strategy-factory.js +44 -0
- package/dist/cli/ai-generation-strategy-factory.js.map +1 -0
- package/dist/cli/ai-generation-strategy.d.ts +33 -0
- package/dist/cli/ai-generation-strategy.d.ts.map +1 -0
- package/dist/cli/ai-generation-strategy.js +3 -0
- package/dist/cli/ai-generation-strategy.js.map +1 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts +38 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts.map +1 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js +174 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js.map +1 -0
- package/dist/cli/auth-strategy-factory.d.ts +31 -0
- package/dist/cli/auth-strategy-factory.d.ts.map +1 -0
- package/dist/cli/auth-strategy-factory.js +61 -0
- package/dist/cli/auth-strategy-factory.js.map +1 -0
- package/dist/cli/auth-strategy.d.ts +31 -0
- package/dist/cli/auth-strategy.d.ts.map +1 -0
- package/dist/cli/auth-strategy.js +3 -0
- package/dist/cli/auth-strategy.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +6 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +103 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +69 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/instagram/instagram-auth-strategy.d.ts +31 -0
- package/dist/cli/instagram/instagram-auth-strategy.d.ts.map +1 -0
- package/dist/cli/instagram/instagram-auth-strategy.js +505 -0
- package/dist/cli/instagram/instagram-auth-strategy.js.map +1 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts +45 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -0
- package/dist/cli/instagram/instagram-upload-strategy.js +303 -0
- package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -0
- package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -1
- package/dist/cli/s3/s3-upload-strategy.js +7 -3
- package/dist/cli/s3/s3-upload-strategy.js.map +1 -1
- package/dist/cli/upload-strategy-factory.d.ts +1 -1
- package/dist/cli/upload-strategy-factory.d.ts.map +1 -1
- package/dist/cli/upload-strategy-factory.js +5 -5
- package/dist/cli/upload-strategy-factory.js.map +1 -1
- package/dist/cli/youtube/youtube-auth-strategy.d.ts +11 -0
- package/dist/cli/youtube/youtube-auth-strategy.d.ts.map +1 -0
- package/dist/cli/youtube/youtube-auth-strategy.js +320 -0
- package/dist/cli/youtube/youtube-auth-strategy.js.map +1 -0
- package/dist/cli/youtube/youtube-upload-strategy.d.ts +10 -3
- package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -1
- package/dist/cli/youtube/youtube-upload-strategy.js +96 -16
- package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -1
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/html-project-parser.d.ts +40 -1
- package/dist/html-project-parser.d.ts.map +1 -1
- package/dist/html-project-parser.js +343 -9
- package/dist/html-project-parser.js.map +1 -1
- package/dist/lib/file.d.ts +2 -0
- package/dist/lib/file.d.ts.map +1 -0
- package/dist/lib/file.js +13 -0
- package/dist/lib/file.js.map +1 -0
- package/dist/lib/net.d.ts +19 -0
- package/dist/lib/net.d.ts.map +1 -0
- package/dist/lib/net.js +101 -0
- package/dist/lib/net.js.map +1 -0
- package/dist/project.d.ts +5 -2
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +9 -1
- package/dist/project.js.map +1 -1
- package/dist/type.d.ts +17 -0
- package/dist/type.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/asset-manager.ts +4 -0
- package/src/cli/ai-generation-strategy-factory.ts +48 -0
- package/src/cli/ai-generation-strategy.ts +35 -0
- package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +266 -0
- package/src/cli/auth-strategy-factory.ts +67 -0
- package/src/cli/auth-strategy.ts +37 -0
- package/src/cli/commands/auth.ts +120 -0
- package/src/cli/commands/generate.ts +55 -2
- package/src/cli/instagram/instagram-auth-strategy.ts +569 -0
- package/src/cli/instagram/instagram-upload-strategy.ts +398 -0
- package/src/cli/s3/s3-upload-strategy.ts +7 -3
- package/src/cli/upload-strategy-factory.ts +6 -9
- package/src/cli/youtube/youtube-auth-strategy.ts +323 -0
- package/src/cli/youtube/youtube-upload-strategy.ts +147 -16
- package/src/cli.ts +2 -4
- package/src/html-project-parser.ts +429 -8
- package/src/lib/file.ts +11 -0
- package/src/lib/net.ts +120 -0
- package/src/project.ts +10 -0
- package/src/type.ts +19 -0
- package/dist/cli/youtube/auth-commands.d.ts +0 -3
- package/dist/cli/youtube/auth-commands.d.ts.map +0 -1
- package/dist/cli/youtube/auth-commands.js +0 -273
- package/dist/cli/youtube/auth-commands.js.map +0 -1
- package/dist/cli/youtube/cli.d.ts +0 -7
- package/dist/cli/youtube/cli.d.ts.map +0 -1
- package/dist/cli/youtube/cli.js +0 -13
- package/dist/cli/youtube/cli.js.map +0 -1
- package/dist/cli/youtube/upload-handler.d.ts +0 -12
- package/dist/cli/youtube/upload-handler.d.ts.map +0 -1
- package/dist/cli/youtube/upload-handler.js +0 -66
- package/dist/cli/youtube/upload-handler.js.map +0 -1
- package/src/cli/youtube/auth-commands.ts +0 -312
- package/src/cli/youtube/cli.ts +0 -11
- package/src/cli/youtube/upload-handler.ts +0 -101
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Container,
|
|
10
10
|
FFmpegOption,
|
|
11
11
|
Upload,
|
|
12
|
+
AIProvider,
|
|
12
13
|
} from './type';
|
|
13
14
|
import { execFile } from 'child_process';
|
|
14
15
|
import { promisify } from 'util';
|
|
@@ -42,7 +43,60 @@ export class HTMLProjectParser {
|
|
|
42
43
|
this.projectDir = dirname(projectPath);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Extracts AI asset generation requirements without full parsing
|
|
48
|
+
* Used to generate AI assets before full project parsing
|
|
49
|
+
*/
|
|
50
|
+
public extractAIGenerationRequirements(): {
|
|
51
|
+
providers: Map<string, AIProvider>;
|
|
52
|
+
assetsToGenerate: Array<{
|
|
53
|
+
name: string;
|
|
54
|
+
path: string;
|
|
55
|
+
integrationName: string;
|
|
56
|
+
prompt: string;
|
|
57
|
+
duration?: number;
|
|
58
|
+
}>;
|
|
59
|
+
} {
|
|
60
|
+
const providers = this.processAIProviders();
|
|
61
|
+
const assetsToGenerate: Array<{
|
|
62
|
+
name: string;
|
|
63
|
+
path: string;
|
|
64
|
+
integrationName: string;
|
|
65
|
+
prompt: string;
|
|
66
|
+
duration?: number;
|
|
67
|
+
}> = [];
|
|
68
|
+
|
|
69
|
+
const assetElements = this.findAssetElements();
|
|
70
|
+
|
|
71
|
+
for (const element of assetElements) {
|
|
72
|
+
const attrs = getAttrs(element);
|
|
73
|
+
const name = attrs.get('data-name') || attrs.get('id');
|
|
74
|
+
const relativePath = attrs.get('data-path') || attrs.get('src');
|
|
75
|
+
|
|
76
|
+
if (!name || !relativePath) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const absolutePath = resolve(this.projectDir, relativePath);
|
|
81
|
+
const aiConfig = this.extractAssetAIConfig(element);
|
|
82
|
+
|
|
83
|
+
// Only include if it has AI config and file doesn't exist
|
|
84
|
+
if (aiConfig && !existsSync(absolutePath)) {
|
|
85
|
+
assetsToGenerate.push({
|
|
86
|
+
name,
|
|
87
|
+
path: absolutePath,
|
|
88
|
+
integrationName: aiConfig.integrationName,
|
|
89
|
+
prompt: aiConfig.prompt,
|
|
90
|
+
duration: aiConfig.duration,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { providers, assetsToGenerate };
|
|
96
|
+
}
|
|
97
|
+
|
|
45
98
|
public async parse(): Promise<Project> {
|
|
99
|
+
const aiProviders = this.processAIProviders();
|
|
46
100
|
const assets = await this.processAssets();
|
|
47
101
|
|
|
48
102
|
// Preflight check: verify all assets exist
|
|
@@ -50,8 +104,9 @@ export class HTMLProjectParser {
|
|
|
50
104
|
|
|
51
105
|
const outputs = this.processOutputs();
|
|
52
106
|
const ffmpegOptions = this.processFfmpegOptions();
|
|
53
|
-
const uploads = this.processUploads();
|
|
54
107
|
const title = this.processTitle();
|
|
108
|
+
const globalTags = this.processGlobalTags();
|
|
109
|
+
const uploads = this.processUploads(title, globalTags);
|
|
55
110
|
const sequences = this.processSequences(assets);
|
|
56
111
|
const cssText = this.html.cssText;
|
|
57
112
|
|
|
@@ -61,6 +116,7 @@ export class HTMLProjectParser {
|
|
|
61
116
|
outputs,
|
|
62
117
|
ffmpegOptions,
|
|
63
118
|
uploads,
|
|
119
|
+
aiProviders,
|
|
64
120
|
title,
|
|
65
121
|
cssText,
|
|
66
122
|
this.projectPath,
|
|
@@ -70,11 +126,17 @@ export class HTMLProjectParser {
|
|
|
70
126
|
/**
|
|
71
127
|
* Validates that all asset files exist on the filesystem
|
|
72
128
|
* Throws an error with a list of missing files if any are not found
|
|
129
|
+
* Note: Assets with AI configuration are skipped if they don't exist (will be generated)
|
|
73
130
|
*/
|
|
74
131
|
private validateAssetFiles(assets: Asset[]): void {
|
|
75
132
|
const missingFiles: string[] = [];
|
|
76
133
|
|
|
77
134
|
for (const asset of assets) {
|
|
135
|
+
// Skip validation for assets with AI config (they will be generated if missing)
|
|
136
|
+
if (asset.ai) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
78
140
|
if (!existsSync(asset.path)) {
|
|
79
141
|
missingFiles.push(asset.path);
|
|
80
142
|
}
|
|
@@ -191,6 +253,9 @@ export class HTMLProjectParser {
|
|
|
191
253
|
// Extract author (optional)
|
|
192
254
|
const author = attrs.get('data-author');
|
|
193
255
|
|
|
256
|
+
// Extract AI configuration from child <ai> element (optional)
|
|
257
|
+
const aiConfig = this.extractAssetAIConfig(element);
|
|
258
|
+
|
|
194
259
|
return {
|
|
195
260
|
name,
|
|
196
261
|
path: absolutePath,
|
|
@@ -202,9 +267,80 @@ export class HTMLProjectParser {
|
|
|
202
267
|
hasVideo,
|
|
203
268
|
hasAudio,
|
|
204
269
|
...(author && { author }),
|
|
270
|
+
...(aiConfig && { ai: aiConfig }),
|
|
205
271
|
};
|
|
206
272
|
}
|
|
207
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Extracts AI configuration from an asset's child <ai> element
|
|
276
|
+
*/
|
|
277
|
+
private extractAssetAIConfig(
|
|
278
|
+
element: Element,
|
|
279
|
+
): { integrationName: string; prompt: string; duration?: number } | null {
|
|
280
|
+
// Find first <ai> child
|
|
281
|
+
if (!('children' in element) || !element.children) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const child of element.children) {
|
|
286
|
+
if (child.type === 'tag' && child.name === 'ai') {
|
|
287
|
+
const aiElement = child as Element;
|
|
288
|
+
const attrs = getAttrs(aiElement);
|
|
289
|
+
|
|
290
|
+
const integrationName = attrs.get('data-integration-name');
|
|
291
|
+
if (!integrationName) {
|
|
292
|
+
console.warn('Asset <ai> element missing data-integration-name attribute');
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Extract prompt and duration from child elements
|
|
297
|
+
let prompt = '';
|
|
298
|
+
let duration: number | undefined;
|
|
299
|
+
|
|
300
|
+
if ('children' in aiElement && aiElement.children) {
|
|
301
|
+
for (const aiChild of aiElement.children) {
|
|
302
|
+
if (aiChild.type === 'tag') {
|
|
303
|
+
const aiChildElement = aiChild as Element;
|
|
304
|
+
|
|
305
|
+
if (aiChildElement.name === 'prompt') {
|
|
306
|
+
if ('children' in aiChildElement && aiChildElement.children) {
|
|
307
|
+
for (const textNode of aiChildElement.children) {
|
|
308
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
309
|
+
prompt += textNode.data;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} else if (aiChildElement.name === 'duration') {
|
|
314
|
+
const durationAttrs = getAttrs(aiChildElement);
|
|
315
|
+
const durationValue = durationAttrs.get('value');
|
|
316
|
+
if (durationValue) {
|
|
317
|
+
const parsed = parseInt(durationValue, 10);
|
|
318
|
+
if (!isNaN(parsed)) {
|
|
319
|
+
duration = parsed;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
prompt = prompt.trim();
|
|
328
|
+
if (!prompt) {
|
|
329
|
+
console.warn('Asset <ai> element missing <prompt>');
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
integrationName,
|
|
335
|
+
prompt,
|
|
336
|
+
...(duration !== undefined && { duration }),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
208
344
|
/**
|
|
209
345
|
* Infers asset type from tag name or file path
|
|
210
346
|
*/
|
|
@@ -563,7 +699,10 @@ export class HTMLProjectParser {
|
|
|
563
699
|
/**
|
|
564
700
|
* Processes all uploads (YouTube, S3, etc.) from the parsed HTML
|
|
565
701
|
*/
|
|
566
|
-
private processUploads(
|
|
702
|
+
private processUploads(
|
|
703
|
+
projectTitle: string,
|
|
704
|
+
globalTags: string[],
|
|
705
|
+
): Map<string, Upload> {
|
|
567
706
|
const uploadsElements = this.findUploadsElements();
|
|
568
707
|
const uploads = new Map<string, Upload>();
|
|
569
708
|
|
|
@@ -575,9 +714,19 @@ export class HTMLProjectParser {
|
|
|
575
714
|
let upload: Upload | null = null;
|
|
576
715
|
|
|
577
716
|
if (childElement.name === 'youtube') {
|
|
578
|
-
upload = this.parseYouTubeElement(
|
|
717
|
+
upload = this.parseYouTubeElement(
|
|
718
|
+
childElement,
|
|
719
|
+
projectTitle,
|
|
720
|
+
globalTags,
|
|
721
|
+
);
|
|
579
722
|
} else if (childElement.name === 's3') {
|
|
580
723
|
upload = this.parseS3Element(childElement);
|
|
724
|
+
} else if (childElement.name === 'instagram') {
|
|
725
|
+
upload = this.parseInstagramElement(
|
|
726
|
+
childElement,
|
|
727
|
+
projectTitle,
|
|
728
|
+
globalTags,
|
|
729
|
+
);
|
|
581
730
|
}
|
|
582
731
|
|
|
583
732
|
if (upload) {
|
|
@@ -594,7 +743,11 @@ export class HTMLProjectParser {
|
|
|
594
743
|
/**
|
|
595
744
|
* Parses a single <youtube> element
|
|
596
745
|
*/
|
|
597
|
-
private parseYouTubeElement(
|
|
746
|
+
private parseYouTubeElement(
|
|
747
|
+
element: Element,
|
|
748
|
+
projectTitle: string,
|
|
749
|
+
globalTags: string[],
|
|
750
|
+
): Upload | null {
|
|
598
751
|
const attrs = getAttrs(element);
|
|
599
752
|
|
|
600
753
|
const name = attrs.get('name');
|
|
@@ -611,7 +764,7 @@ export class HTMLProjectParser {
|
|
|
611
764
|
let uploadTitle: string | undefined;
|
|
612
765
|
let privacy: 'public' | 'unlisted' | 'private' = 'private';
|
|
613
766
|
let madeForKids = false;
|
|
614
|
-
const
|
|
767
|
+
const localTags: string[] = [];
|
|
615
768
|
let category = 'entertainment';
|
|
616
769
|
let language = 'en';
|
|
617
770
|
let description = '';
|
|
@@ -651,7 +804,7 @@ export class HTMLProjectParser {
|
|
|
651
804
|
const tagAttrs = getAttrs(childElement);
|
|
652
805
|
const tagName = tagAttrs.get('name');
|
|
653
806
|
if (tagName) {
|
|
654
|
-
|
|
807
|
+
localTags.push(tagName);
|
|
655
808
|
}
|
|
656
809
|
break;
|
|
657
810
|
}
|
|
@@ -701,15 +854,21 @@ export class HTMLProjectParser {
|
|
|
701
854
|
}
|
|
702
855
|
}
|
|
703
856
|
|
|
857
|
+
// Merge global tags + local tags (global first)
|
|
858
|
+
const allTags = [...globalTags, ...localTags];
|
|
859
|
+
|
|
860
|
+
// Use project title if upload doesn't have its own
|
|
861
|
+
const finalTitle = uploadTitle || projectTitle;
|
|
862
|
+
|
|
704
863
|
return {
|
|
705
864
|
name,
|
|
706
865
|
tag: element.name, // e.g., "youtube", "s3", etc.
|
|
707
866
|
outputName,
|
|
708
|
-
title:
|
|
867
|
+
title: finalTitle,
|
|
709
868
|
videoId,
|
|
710
869
|
privacy,
|
|
711
870
|
madeForKids,
|
|
712
|
-
tags,
|
|
871
|
+
tags: allTags,
|
|
713
872
|
category,
|
|
714
873
|
language,
|
|
715
874
|
description: description.trim(),
|
|
@@ -796,6 +955,268 @@ export class HTMLProjectParser {
|
|
|
796
955
|
};
|
|
797
956
|
}
|
|
798
957
|
|
|
958
|
+
/**
|
|
959
|
+
* Parses a single <instagram> element
|
|
960
|
+
*/
|
|
961
|
+
private parseInstagramElement(
|
|
962
|
+
element: Element,
|
|
963
|
+
projectTitle: string,
|
|
964
|
+
globalTags: string[],
|
|
965
|
+
): Upload | null {
|
|
966
|
+
const attrs = getAttrs(element);
|
|
967
|
+
|
|
968
|
+
const name = attrs.get('name');
|
|
969
|
+
const outputName = attrs.get('data-output-name');
|
|
970
|
+
|
|
971
|
+
if (!name || !outputName) {
|
|
972
|
+
console.warn('Instagram upload missing name or data-output-name attribute');
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Parse Instagram-specific child elements
|
|
977
|
+
let caption = '';
|
|
978
|
+
let shareToFeed = false;
|
|
979
|
+
let thumbOffset: number | undefined;
|
|
980
|
+
let coverUrl: string | undefined;
|
|
981
|
+
let videoUrl: string | undefined;
|
|
982
|
+
const localTags: string[] = [];
|
|
983
|
+
|
|
984
|
+
if ('children' in element && element.children) {
|
|
985
|
+
for (const child of element.children) {
|
|
986
|
+
if (child.type === 'tag') {
|
|
987
|
+
const childElement = child as Element;
|
|
988
|
+
const childAttrs = getAttrs(childElement);
|
|
989
|
+
|
|
990
|
+
switch (childElement.name) {
|
|
991
|
+
case 'pre': {
|
|
992
|
+
// Get text content (unified with YouTube description)
|
|
993
|
+
if ('children' in childElement && childElement.children) {
|
|
994
|
+
for (const textNode of childElement.children) {
|
|
995
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
996
|
+
caption += textNode.data;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
case 'caption': {
|
|
1003
|
+
// Legacy syntax (deprecated, but still supported)
|
|
1004
|
+
if ('children' in childElement && childElement.children) {
|
|
1005
|
+
for (const textNode of childElement.children) {
|
|
1006
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
1007
|
+
caption += textNode.data;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
caption = caption.trim();
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
case 'share-to-feed': {
|
|
1015
|
+
shareToFeed = true;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
case 'thumbnail': {
|
|
1019
|
+
// Unified syntax like YouTube: <thumbnail data-timecode="1000ms" />
|
|
1020
|
+
const timecode = childAttrs.get('data-timecode');
|
|
1021
|
+
if (timecode) {
|
|
1022
|
+
// Parse timecode (e.g., "1000ms" or "1s")
|
|
1023
|
+
const match = timecode.match(/^(\d+(?:\.\d+)?)(ms|s)$/);
|
|
1024
|
+
if (match) {
|
|
1025
|
+
const value = parseFloat(match[1]);
|
|
1026
|
+
const unit = match[2];
|
|
1027
|
+
thumbOffset = unit === 's' ? value * 1000 : value;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
case 'thumb-offset': {
|
|
1033
|
+
// Legacy syntax (deprecated, but still supported)
|
|
1034
|
+
const offset = childAttrs.get('value');
|
|
1035
|
+
if (offset) {
|
|
1036
|
+
thumbOffset = parseInt(offset, 10);
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
case 'cover-url': {
|
|
1041
|
+
coverUrl = childAttrs.get('value');
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
case 'video-url': {
|
|
1045
|
+
videoUrl = childAttrs.get('value');
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
case 'tag': {
|
|
1049
|
+
const tagName = childAttrs.get('name');
|
|
1050
|
+
if (tagName) {
|
|
1051
|
+
localTags.push(tagName);
|
|
1052
|
+
}
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Merge global tags + local tags
|
|
1061
|
+
const allTags = [...globalTags, ...localTags];
|
|
1062
|
+
|
|
1063
|
+
return {
|
|
1064
|
+
name,
|
|
1065
|
+
tag: element.name, // "instagram"
|
|
1066
|
+
outputName,
|
|
1067
|
+
title: projectTitle,
|
|
1068
|
+
privacy: 'private', // Default values (not used but required by Upload type)
|
|
1069
|
+
madeForKids: false,
|
|
1070
|
+
tags: allTags,
|
|
1071
|
+
category: '',
|
|
1072
|
+
language: '',
|
|
1073
|
+
description: '',
|
|
1074
|
+
instagram: {
|
|
1075
|
+
caption, // Raw caption with ${variables}, will be rendered by upload strategy
|
|
1076
|
+
shareToFeed,
|
|
1077
|
+
thumbOffset,
|
|
1078
|
+
coverUrl,
|
|
1079
|
+
videoUrl,
|
|
1080
|
+
},
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Processes all AI providers from the parsed HTML
|
|
1086
|
+
*/
|
|
1087
|
+
private processAIProviders(): Map<string, AIProvider> {
|
|
1088
|
+
const aiElements = this.findAIElements();
|
|
1089
|
+
const providers = new Map<string, AIProvider>();
|
|
1090
|
+
|
|
1091
|
+
for (const aiElement of aiElements) {
|
|
1092
|
+
if ('children' in aiElement && aiElement.children) {
|
|
1093
|
+
for (const child of aiElement.children) {
|
|
1094
|
+
if (child.type === 'tag') {
|
|
1095
|
+
const childElement = child as Element;
|
|
1096
|
+
const provider = this.parseAIProviderElement(childElement);
|
|
1097
|
+
|
|
1098
|
+
if (provider) {
|
|
1099
|
+
providers.set(provider.name, provider);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return providers;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Parses a single AI provider element (e.g., <music-api-ai>)
|
|
1111
|
+
*/
|
|
1112
|
+
private parseAIProviderElement(element: Element): AIProvider | null {
|
|
1113
|
+
const attrs = getAttrs(element);
|
|
1114
|
+
|
|
1115
|
+
const name = attrs.get('name');
|
|
1116
|
+
if (!name) {
|
|
1117
|
+
console.warn('AI provider missing name attribute');
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const tag = element.name; // e.g., "music-api-ai"
|
|
1122
|
+
|
|
1123
|
+
// Extract optional model from child elements
|
|
1124
|
+
let model: string | undefined;
|
|
1125
|
+
|
|
1126
|
+
if ('children' in element && element.children) {
|
|
1127
|
+
for (const child of element.children) {
|
|
1128
|
+
if (child.type === 'tag' && child.name === 'model') {
|
|
1129
|
+
const childElement = child as Element;
|
|
1130
|
+
const childAttrs = getAttrs(childElement);
|
|
1131
|
+
model = childAttrs.get('name');
|
|
1132
|
+
break; // Only use first model element
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return {
|
|
1138
|
+
name,
|
|
1139
|
+
tag,
|
|
1140
|
+
...(model && { model }),
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Finds all <ai> configuration elements in the HTML (excludes <ai> elements inside <asset>)
|
|
1146
|
+
*/
|
|
1147
|
+
private findAIElements(): Element[] {
|
|
1148
|
+
const results: Element[] = [];
|
|
1149
|
+
|
|
1150
|
+
const traverse = (node: ASTNode, insideAsset: boolean = false) => {
|
|
1151
|
+
if (node.type === 'tag') {
|
|
1152
|
+
const element = node as Element;
|
|
1153
|
+
|
|
1154
|
+
// Track if we're entering an <asset> element
|
|
1155
|
+
const isAsset = element.name === 'asset';
|
|
1156
|
+
const nowInsideAsset = insideAsset || isAsset;
|
|
1157
|
+
|
|
1158
|
+
// Only collect <ai> elements that are NOT inside <asset> elements
|
|
1159
|
+
if (element.name === 'ai' && !insideAsset) {
|
|
1160
|
+
results.push(element);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Continue traversing children with updated insideAsset flag
|
|
1164
|
+
if ('children' in element && element.children) {
|
|
1165
|
+
for (const child of element.children) {
|
|
1166
|
+
traverse(child, nowInsideAsset);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
} else if ('children' in node && node.children) {
|
|
1170
|
+
for (const child of node.children) {
|
|
1171
|
+
traverse(child, insideAsset);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
traverse(this.html.ast);
|
|
1177
|
+
return results;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Processes global tags from the top of the HTML file (before <project>)
|
|
1182
|
+
*/
|
|
1183
|
+
private processGlobalTags(): string[] {
|
|
1184
|
+
const tags: string[] = [];
|
|
1185
|
+
|
|
1186
|
+
const traverse = (node: ASTNode, insideProject: boolean = false) => {
|
|
1187
|
+
if (node.type === 'tag') {
|
|
1188
|
+
const element = node as Element;
|
|
1189
|
+
|
|
1190
|
+
// Stop when we hit <project>, <uploads>, or <outputs>
|
|
1191
|
+
if (
|
|
1192
|
+
element.name === 'project' ||
|
|
1193
|
+
element.name === 'uploads' ||
|
|
1194
|
+
element.name === 'outputs'
|
|
1195
|
+
) {
|
|
1196
|
+
insideProject = true;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Only parse <tag> elements outside of project/uploads/outputs
|
|
1200
|
+
if (!insideProject && element.name === 'tag') {
|
|
1201
|
+
const attrs = getAttrs(element);
|
|
1202
|
+
const tagName = attrs.get('name');
|
|
1203
|
+
if (tagName) {
|
|
1204
|
+
tags.push(tagName);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if ('children' in node && node.children) {
|
|
1210
|
+
for (const child of node.children) {
|
|
1211
|
+
traverse(child, insideProject);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
traverse(this.html.ast);
|
|
1217
|
+
return tags;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
799
1220
|
/**
|
|
800
1221
|
* Processes the title from the parsed HTML
|
|
801
1222
|
*/
|
package/src/lib/file.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
export function writeFile(filePath: string, buffer: Buffer) {
|
|
5
|
+
const dir = dirname(filePath);
|
|
6
|
+
if (!existsSync(dir)) {
|
|
7
|
+
mkdirSync(dir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
writeFileSync(filePath, buffer);
|
|
11
|
+
}
|
package/src/lib/net.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
|
|
4
|
+
export interface HttpRequestOptions {
|
|
5
|
+
url: string;
|
|
6
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: unknown | URLSearchParams;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Makes an HTTP/HTTPS request
|
|
13
|
+
* @param options Request options
|
|
14
|
+
* @returns Parsed JSON response
|
|
15
|
+
*/
|
|
16
|
+
export async function makeRequest<T>(options: HttpRequestOptions): Promise<T> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const urlObj = new URL(options.url);
|
|
19
|
+
const protocol = urlObj.protocol === 'https:' ? https : http;
|
|
20
|
+
|
|
21
|
+
// Determine content type and serialize body
|
|
22
|
+
let bodyString: string | undefined;
|
|
23
|
+
let defaultContentType: string;
|
|
24
|
+
|
|
25
|
+
if (options.body) {
|
|
26
|
+
if (options.body instanceof URLSearchParams) {
|
|
27
|
+
bodyString = options.body.toString();
|
|
28
|
+
defaultContentType = 'application/x-www-form-urlencoded';
|
|
29
|
+
} else {
|
|
30
|
+
bodyString = JSON.stringify(options.body);
|
|
31
|
+
defaultContentType = 'application/json';
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
defaultContentType = 'application/json';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const requestOptions = {
|
|
38
|
+
hostname: urlObj.hostname,
|
|
39
|
+
port: urlObj.port || (protocol === https ? 443 : 80),
|
|
40
|
+
path: urlObj.pathname + urlObj.search,
|
|
41
|
+
method: options.method,
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': defaultContentType,
|
|
44
|
+
...options.headers,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const req = protocol.request(requestOptions, (res) => {
|
|
49
|
+
const chunks: Buffer[] = [];
|
|
50
|
+
|
|
51
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
52
|
+
res.on('end', () => {
|
|
53
|
+
try {
|
|
54
|
+
const responseText = Buffer.concat(chunks).toString('utf-8');
|
|
55
|
+
const data = JSON.parse(responseText) as T;
|
|
56
|
+
|
|
57
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
58
|
+
reject(
|
|
59
|
+
new Error(
|
|
60
|
+
`HTTP request failed with status ${res.statusCode}: ${responseText}`,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
resolve(data);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
reject(
|
|
69
|
+
new Error(
|
|
70
|
+
`Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
req.on('error', (error) => {
|
|
78
|
+
reject(new Error(`Request failed: ${error.message}`));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (bodyString) {
|
|
82
|
+
req.write(bodyString);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
req.end();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Downloads a file from URL to buffer
|
|
91
|
+
* @param url URL to download from
|
|
92
|
+
* @returns Buffer containing the file data
|
|
93
|
+
*/
|
|
94
|
+
export async function downloadFile(url: string): Promise<Buffer> {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
97
|
+
|
|
98
|
+
protocol
|
|
99
|
+
.get(url, (response) => {
|
|
100
|
+
if (response.statusCode !== 200) {
|
|
101
|
+
reject(
|
|
102
|
+
new Error(`Download failed: HTTP ${response.statusCode}`),
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const chunks: Buffer[] = [];
|
|
108
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
109
|
+
response.on('end', () => {
|
|
110
|
+
try {
|
|
111
|
+
const buffer = Buffer.concat(chunks);
|
|
112
|
+
resolve(buffer);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
reject(error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
})
|
|
118
|
+
.on('error', reject);
|
|
119
|
+
});
|
|
120
|
+
}
|
package/src/project.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
SequenceDefinition,
|
|
5
5
|
FFmpegOption,
|
|
6
6
|
Upload,
|
|
7
|
+
AIProvider,
|
|
7
8
|
} from './type';
|
|
8
9
|
import { Label } from './ffmpeg';
|
|
9
10
|
import { AssetManager } from './asset-manager';
|
|
@@ -23,6 +24,7 @@ export class Project {
|
|
|
23
24
|
private outputs: Map<string, Output>,
|
|
24
25
|
private ffmpegOptions: Map<string, FFmpegOption>,
|
|
25
26
|
private uploads: Map<string, Upload>,
|
|
27
|
+
private aiProviders: Map<string, AIProvider>,
|
|
26
28
|
private title: string,
|
|
27
29
|
private cssText: string,
|
|
28
30
|
private projectPath: string,
|
|
@@ -127,6 +129,14 @@ export class Project {
|
|
|
127
129
|
return this.getUpload(name);
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
public getAIProviders(): Map<string, AIProvider> {
|
|
133
|
+
return this.aiProviders;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public getAIProvider(name: string): AIProvider | undefined {
|
|
137
|
+
return this.aiProviders.get(name);
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
public getTitle(): string {
|
|
131
141
|
return this.title;
|
|
132
142
|
}
|