@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.
Files changed (111) hide show
  1. package/Makefile +20 -0
  2. package/dist/asset-manager.d.ts +1 -0
  3. package/dist/asset-manager.d.ts.map +1 -1
  4. package/dist/asset-manager.js +3 -0
  5. package/dist/asset-manager.js.map +1 -1
  6. package/dist/cli/ai-generation-strategy-factory.d.ts +23 -0
  7. package/dist/cli/ai-generation-strategy-factory.d.ts.map +1 -0
  8. package/dist/cli/ai-generation-strategy-factory.js +44 -0
  9. package/dist/cli/ai-generation-strategy-factory.js.map +1 -0
  10. package/dist/cli/ai-generation-strategy.d.ts +33 -0
  11. package/dist/cli/ai-generation-strategy.d.ts.map +1 -0
  12. package/dist/cli/ai-generation-strategy.js +3 -0
  13. package/dist/cli/ai-generation-strategy.js.map +1 -0
  14. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts +38 -0
  15. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts.map +1 -0
  16. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js +174 -0
  17. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js.map +1 -0
  18. package/dist/cli/auth-strategy-factory.d.ts +31 -0
  19. package/dist/cli/auth-strategy-factory.d.ts.map +1 -0
  20. package/dist/cli/auth-strategy-factory.js +61 -0
  21. package/dist/cli/auth-strategy-factory.js.map +1 -0
  22. package/dist/cli/auth-strategy.d.ts +31 -0
  23. package/dist/cli/auth-strategy.d.ts.map +1 -0
  24. package/dist/cli/auth-strategy.js +3 -0
  25. package/dist/cli/auth-strategy.js.map +1 -0
  26. package/dist/cli/commands/auth.d.ts +6 -0
  27. package/dist/cli/commands/auth.d.ts.map +1 -0
  28. package/dist/cli/commands/auth.js +103 -0
  29. package/dist/cli/commands/auth.js.map +1 -0
  30. package/dist/cli/commands/generate.d.ts.map +1 -1
  31. package/dist/cli/commands/generate.js +69 -2
  32. package/dist/cli/commands/generate.js.map +1 -1
  33. package/dist/cli/instagram/instagram-auth-strategy.d.ts +31 -0
  34. package/dist/cli/instagram/instagram-auth-strategy.d.ts.map +1 -0
  35. package/dist/cli/instagram/instagram-auth-strategy.js +505 -0
  36. package/dist/cli/instagram/instagram-auth-strategy.js.map +1 -0
  37. package/dist/cli/instagram/instagram-upload-strategy.d.ts +45 -0
  38. package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -0
  39. package/dist/cli/instagram/instagram-upload-strategy.js +303 -0
  40. package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -0
  41. package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -1
  42. package/dist/cli/s3/s3-upload-strategy.js +7 -3
  43. package/dist/cli/s3/s3-upload-strategy.js.map +1 -1
  44. package/dist/cli/upload-strategy-factory.d.ts +1 -1
  45. package/dist/cli/upload-strategy-factory.d.ts.map +1 -1
  46. package/dist/cli/upload-strategy-factory.js +5 -5
  47. package/dist/cli/upload-strategy-factory.js.map +1 -1
  48. package/dist/cli/youtube/youtube-auth-strategy.d.ts +11 -0
  49. package/dist/cli/youtube/youtube-auth-strategy.d.ts.map +1 -0
  50. package/dist/cli/youtube/youtube-auth-strategy.js +320 -0
  51. package/dist/cli/youtube/youtube-auth-strategy.js.map +1 -0
  52. package/dist/cli/youtube/youtube-upload-strategy.d.ts +10 -3
  53. package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -1
  54. package/dist/cli/youtube/youtube-upload-strategy.js +96 -16
  55. package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -1
  56. package/dist/cli.js +2 -3
  57. package/dist/cli.js.map +1 -1
  58. package/dist/html-project-parser.d.ts +40 -1
  59. package/dist/html-project-parser.d.ts.map +1 -1
  60. package/dist/html-project-parser.js +343 -9
  61. package/dist/html-project-parser.js.map +1 -1
  62. package/dist/lib/file.d.ts +2 -0
  63. package/dist/lib/file.d.ts.map +1 -0
  64. package/dist/lib/file.js +13 -0
  65. package/dist/lib/file.js.map +1 -0
  66. package/dist/lib/net.d.ts +19 -0
  67. package/dist/lib/net.d.ts.map +1 -0
  68. package/dist/lib/net.js +101 -0
  69. package/dist/lib/net.js.map +1 -0
  70. package/dist/project.d.ts +5 -2
  71. package/dist/project.d.ts.map +1 -1
  72. package/dist/project.js +9 -1
  73. package/dist/project.js.map +1 -1
  74. package/dist/type.d.ts +17 -0
  75. package/dist/type.d.ts.map +1 -1
  76. package/package.json +2 -1
  77. package/src/asset-manager.ts +4 -0
  78. package/src/cli/ai-generation-strategy-factory.ts +48 -0
  79. package/src/cli/ai-generation-strategy.ts +35 -0
  80. package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +266 -0
  81. package/src/cli/auth-strategy-factory.ts +67 -0
  82. package/src/cli/auth-strategy.ts +37 -0
  83. package/src/cli/commands/auth.ts +120 -0
  84. package/src/cli/commands/generate.ts +55 -2
  85. package/src/cli/instagram/instagram-auth-strategy.ts +569 -0
  86. package/src/cli/instagram/instagram-upload-strategy.ts +398 -0
  87. package/src/cli/s3/s3-upload-strategy.ts +7 -3
  88. package/src/cli/upload-strategy-factory.ts +6 -9
  89. package/src/cli/youtube/youtube-auth-strategy.ts +323 -0
  90. package/src/cli/youtube/youtube-upload-strategy.ts +147 -16
  91. package/src/cli.ts +2 -4
  92. package/src/html-project-parser.ts +429 -8
  93. package/src/lib/file.ts +11 -0
  94. package/src/lib/net.ts +120 -0
  95. package/src/project.ts +10 -0
  96. package/src/type.ts +19 -0
  97. package/dist/cli/youtube/auth-commands.d.ts +0 -3
  98. package/dist/cli/youtube/auth-commands.d.ts.map +0 -1
  99. package/dist/cli/youtube/auth-commands.js +0 -273
  100. package/dist/cli/youtube/auth-commands.js.map +0 -1
  101. package/dist/cli/youtube/cli.d.ts +0 -7
  102. package/dist/cli/youtube/cli.d.ts.map +0 -1
  103. package/dist/cli/youtube/cli.js +0 -13
  104. package/dist/cli/youtube/cli.js.map +0 -1
  105. package/dist/cli/youtube/upload-handler.d.ts +0 -12
  106. package/dist/cli/youtube/upload-handler.d.ts.map +0 -1
  107. package/dist/cli/youtube/upload-handler.js +0 -66
  108. package/dist/cli/youtube/upload-handler.js.map +0 -1
  109. package/src/cli/youtube/auth-commands.ts +0 -312
  110. package/src/cli/youtube/cli.ts +0 -11
  111. 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(): Map<string, Upload> {
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(childElement);
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(element: Element): Upload | null {
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 tags: string[] = [];
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
- tags.push(tagName);
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: uploadTitle,
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
  */
@@ -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
  }