@gannochenko/staticstripes 0.0.22 → 0.0.23

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 (45) hide show
  1. package/dist/app-builder.d.ts +18 -0
  2. package/dist/app-builder.d.ts.map +1 -0
  3. package/dist/app-builder.js +94 -0
  4. package/dist/app-builder.js.map +1 -0
  5. package/dist/cli/commands/filters.d.ts +3 -0
  6. package/dist/cli/commands/filters.d.ts.map +1 -0
  7. package/dist/cli/commands/filters.js +21 -0
  8. package/dist/cli/commands/filters.js.map +1 -0
  9. package/dist/cli/commands/generate.d.ts.map +1 -1
  10. package/dist/cli/commands/generate.js +6 -1
  11. package/dist/cli/commands/generate.js.map +1 -1
  12. package/dist/cli/instagram/instagram-upload-strategy.d.ts +5 -0
  13. package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -1
  14. package/dist/cli/instagram/instagram-upload-strategy.js +46 -3
  15. package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -1
  16. package/dist/cli.js +2 -0
  17. package/dist/cli.js.map +1 -1
  18. package/dist/html-project-parser.d.ts +20 -1
  19. package/dist/html-project-parser.d.ts.map +1 -1
  20. package/dist/html-project-parser.js +128 -15
  21. package/dist/html-project-parser.js.map +1 -1
  22. package/dist/project.d.ts +4 -1
  23. package/dist/project.d.ts.map +1 -1
  24. package/dist/project.js +50 -1
  25. package/dist/project.js.map +1 -1
  26. package/dist/sample-sequences.d.ts.map +1 -1
  27. package/dist/sample-sequences.js +7 -0
  28. package/dist/sample-sequences.js.map +1 -1
  29. package/dist/sequence.d.ts +4 -1
  30. package/dist/sequence.d.ts.map +1 -1
  31. package/dist/sequence.js +43 -19
  32. package/dist/sequence.js.map +1 -1
  33. package/dist/type.d.ts +18 -1
  34. package/dist/type.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/app-builder.ts +113 -0
  37. package/src/cli/commands/filters.ts +21 -0
  38. package/src/cli/commands/generate.ts +10 -1
  39. package/src/cli/instagram/instagram-upload-strategy.ts +61 -1
  40. package/src/cli.ts +2 -0
  41. package/src/html-project-parser.ts +172 -26
  42. package/src/project.ts +62 -0
  43. package/src/sample-sequences.ts +7 -0
  44. package/src/sequence.ts +50 -20
  45. package/src/type.ts +20 -1
@@ -35,7 +35,7 @@ export class InstagramUploadStrategy implements UploadStrategy {
35
35
  );
36
36
  }
37
37
 
38
- const { caption, shareToFeed, thumbOffset, coverUrl, videoUrl } =
38
+ const { caption, shareToFeed, thumbOffset, coverUrl, videoUrl, locationId } =
39
39
  upload.instagram;
40
40
 
41
41
  // Load credentials from local .auth/<upload-name>.json or global ~/.staticstripes/auth/<upload-name>.json
@@ -120,6 +120,15 @@ export class InstagramUploadStrategy implements UploadStrategy {
120
120
  }
121
121
  console.log('');
122
122
 
123
+ // Resolve location ID if search query is provided
124
+ let resolvedLocationId = locationId;
125
+ if (locationId && locationId.startsWith('search:')) {
126
+ const searchQuery = locationId.substring(7); // Remove "search:" prefix
127
+ console.log(`šŸ“ Searching for location: ${searchQuery}...`);
128
+ resolvedLocationId = await this.searchLocation(credentials, searchQuery);
129
+ console.log(`āœ… Found location ID: ${resolvedLocationId}\n`);
130
+ }
131
+
123
132
  // Step 1: Create media container
124
133
  console.log('šŸ“¦ Step 1: Creating media container...');
125
134
  const containerId = await this.createMediaContainer(
@@ -129,6 +138,7 @@ export class InstagramUploadStrategy implements UploadStrategy {
129
138
  shareToFeed,
130
139
  thumbOffset,
131
140
  coverUrl,
141
+ resolvedLocationId,
132
142
  );
133
143
 
134
144
  console.log(`āœ… Container created: ${containerId}`);
@@ -150,6 +160,51 @@ export class InstagramUploadStrategy implements UploadStrategy {
150
160
  console.log(`šŸ“ŗ View at: ${permalink}\n`);
151
161
  }
152
162
 
163
+ /**
164
+ * Searches for a location by city and country name
165
+ * Returns the location ID from Instagram's location database
166
+ */
167
+ private async searchLocation(
168
+ credentials: InstagramCredentials,
169
+ searchQuery: string,
170
+ ): Promise<string> {
171
+ const params = new URLSearchParams({
172
+ q: searchQuery,
173
+ fields: 'id,name',
174
+ access_token: credentials.accessToken,
175
+ });
176
+
177
+ const url = `${this.GRAPH_API_BASE}/${this.API_VERSION}/${credentials.igUserId}/locations?${params.toString()}`;
178
+
179
+ try {
180
+ const data = await makeRequest<{
181
+ data?: Array<{ id: string; name: string }>;
182
+ }>({
183
+ url,
184
+ method: 'GET',
185
+ });
186
+
187
+ if (!data.data || data.data.length === 0) {
188
+ throw new Error(
189
+ `No locations found for "${searchQuery}"\n\n` +
190
+ `Tip: Try different variations of the city/country name\n` +
191
+ `Example: "Paris, France" or "New York, USA"`,
192
+ );
193
+ }
194
+
195
+ // Return the first (most relevant) result
196
+ const location = data.data[0];
197
+ console.log(` Found: ${location.name} (ID: ${location.id})`);
198
+ return location.id;
199
+ } catch (error) {
200
+ throw new Error(
201
+ `āŒ Error: Failed to search for location "${searchQuery}"\n` +
202
+ `${error instanceof Error ? error.message : String(error)}\n\n` +
203
+ `Note: Location search requires a valid access token and may not be available in all regions.`,
204
+ );
205
+ }
206
+ }
207
+
153
208
  /**
154
209
  * Creates a media container for the Reel
155
210
  */
@@ -160,6 +215,7 @@ export class InstagramUploadStrategy implements UploadStrategy {
160
215
  shareToFeed: boolean,
161
216
  thumbOffset?: number,
162
217
  coverUrl?: string,
218
+ locationId?: string,
163
219
  ): Promise<string> {
164
220
  const params = new URLSearchParams({
165
221
  media_type: 'REELS',
@@ -180,6 +236,10 @@ export class InstagramUploadStrategy implements UploadStrategy {
180
236
  params.append('cover_url', coverUrl);
181
237
  }
182
238
 
239
+ if (locationId) {
240
+ params.append('location_id', locationId);
241
+ }
242
+
183
243
  const url = `${this.GRAPH_API_BASE}/${this.API_VERSION}/${credentials.igUserId}/media`;
184
244
 
185
245
  try {
package/src/cli.ts CHANGED
@@ -8,6 +8,7 @@ import { registerBootstrapCommand } from './cli/commands/bootstrap.js';
8
8
  import { registerAddAssetsCommand } from './cli/commands/add-assets.js';
9
9
  import { registerUploadCommand } from './cli/commands/upload.js';
10
10
  import { registerAuthCommand } from './cli/commands/auth.js';
11
+ import { registerFiltersCommand } from './cli/commands/filters.js';
11
12
 
12
13
  // Read version from package.json
13
14
  // In built code, this file is at dist/cli.js, package.json is at ../package.json
@@ -65,5 +66,6 @@ registerBootstrapCommand(program, handleError);
65
66
  registerAddAssetsCommand(program, handleError);
66
67
  registerUploadCommand(program, handleError);
67
68
  registerAuthCommand(program, handleError);
69
+ registerFiltersCommand(program);
68
70
 
69
71
  program.parse(process.argv);
@@ -1033,6 +1033,7 @@ export class HTMLProjectParser {
1033
1033
  let thumbOffset: number | undefined;
1034
1034
  let coverUrl: string | undefined;
1035
1035
  let videoUrl: string | undefined;
1036
+ let locationId: string | undefined;
1036
1037
  const localTags: string[] = [];
1037
1038
 
1038
1039
  if ('children' in element && element.children) {
@@ -1106,6 +1107,21 @@ export class HTMLProjectParser {
1106
1107
  }
1107
1108
  break;
1108
1109
  }
1110
+ case 'location': {
1111
+ const id = childAttrs.get('id');
1112
+ const city = childAttrs.get('city');
1113
+ const country = childAttrs.get('country');
1114
+
1115
+ if (id) {
1116
+ // Explicit location ID provided
1117
+ locationId = id;
1118
+ } else if (city && country) {
1119
+ // Store search query for later resolution
1120
+ // Format: "search:City, Country"
1121
+ locationId = `search:${city}, ${country}`;
1122
+ }
1123
+ break;
1124
+ }
1109
1125
  }
1110
1126
  }
1111
1127
  }
@@ -1131,6 +1147,7 @@ export class HTMLProjectParser {
1131
1147
  thumbOffset,
1132
1148
  coverUrl,
1133
1149
  videoUrl,
1150
+ locationId,
1134
1151
  },
1135
1152
  };
1136
1153
  }
@@ -1615,26 +1632,44 @@ export class HTMLProjectParser {
1615
1632
  const container = this.extractFragmentContainer(element);
1616
1633
  const app = container ? undefined : this.extractFragmentApp(element);
1617
1634
 
1618
- // 5. Parse trimLeft from -trim-start property
1619
- const trimLeft = this.parseTrimStart(styles['-trim-start']);
1620
-
1621
- // 5b. Parse trimRight from -trim-end property
1622
- const trimRight = this.parseTrimEnd(styles['-trim-end']);
1623
-
1624
- // 6. Parse duration from -duration property
1625
- const duration = this.parseDurationProperty(
1626
- styles['-duration'],
1627
- assetName,
1628
- assets,
1629
- trimLeft,
1630
- trimRight,
1631
- );
1632
-
1633
- // 7. Parse -offset-start for overlayLeft (can be number or expression)
1634
- const overlayLeft = this.parseOffsetStart(styles['-offset-start']);
1635
-
1636
- // 8. Parse -offset-end for overlayRight (temporary, will be normalized)
1637
- const overlayRight = this.parseOffsetEnd(styles['-offset-end']);
1635
+ // 4b. Parse data-timing attribute (short syntax) - takes precedence over CSS
1636
+ const dataTiming = this.parseDataTiming(attrs.get('data-timing'));
1637
+
1638
+ // 5. Parse trimLeft from data-timing or -trim-start property
1639
+ const trimLeft =
1640
+ dataTiming.trimStart !== undefined
1641
+ ? dataTiming.trimStart
1642
+ : this.parseTrimStart(styles['-trim-start']);
1643
+
1644
+ // 5b. Parse trimRight from data-timing or -trim-end property
1645
+ const trimRight =
1646
+ dataTiming.trimEnd !== undefined
1647
+ ? dataTiming.trimEnd
1648
+ : this.parseTrimEnd(styles['-trim-end']);
1649
+
1650
+ // 6. Parse duration from data-timing or -duration property
1651
+ const duration =
1652
+ dataTiming.duration !== undefined
1653
+ ? dataTiming.duration
1654
+ : this.parseDurationProperty(
1655
+ styles['-duration'],
1656
+ assetName,
1657
+ assets,
1658
+ trimLeft,
1659
+ trimRight,
1660
+ );
1661
+
1662
+ // 7. Parse overlayLeft from data-timing or -offset-start property
1663
+ const overlayLeft =
1664
+ dataTiming.offsetStart !== undefined
1665
+ ? dataTiming.offsetStart
1666
+ : this.parseOffsetStart(styles['-offset-start']);
1667
+
1668
+ // 8. Parse overlayRight from data-timing or -offset-end property
1669
+ const overlayRight =
1670
+ dataTiming.offsetEnd !== undefined
1671
+ ? dataTiming.offsetEnd
1672
+ : this.parseOffsetEnd(styles['-offset-end']);
1638
1673
 
1639
1674
  // 9. Parse -overlay-start-z-index for overlayZIndex
1640
1675
  const overlayZIndex = this.parseZIndex(styles['-overlay-start-z-index']);
@@ -1661,7 +1696,10 @@ export class HTMLProjectParser {
1661
1696
  // 15. Parse filter (for visual filters)
1662
1697
  const visualFilter = this.parseVisualFilterProperty(styles['filter']);
1663
1698
 
1664
- // 16. Extract timecode label from data-timecode attribute
1699
+ // 16. Parse sound property (on/off)
1700
+ const sound = this.parseSoundProperty(styles['-sound']);
1701
+
1702
+ // 17. Extract timecode label from data-timecode attribute
1665
1703
  const timecodeLabel = attrs.get('data-timecode') || undefined;
1666
1704
 
1667
1705
  return {
@@ -1692,6 +1730,7 @@ export class HTMLProjectParser {
1692
1730
  chromakeyBlend: chromakeyData.chromakeyBlend,
1693
1731
  chromakeySimilarity: chromakeyData.chromakeySimilarity,
1694
1732
  chromakeyColor: chromakeyData.chromakeyColor,
1733
+ sound,
1695
1734
  ...(visualFilter && { visualFilter }), // Add visualFilter if present
1696
1735
  ...(container && { container }), // Add container if present
1697
1736
  ...(app && { app }), // Add app if present
@@ -1718,6 +1757,25 @@ export class HTMLProjectParser {
1718
1757
  return trimmed || undefined;
1719
1758
  }
1720
1759
 
1760
+ /**
1761
+ * Parses -sound property
1762
+ * Can be: "on" (default) or "off"
1763
+ * When "off", replaces audio stream with silence
1764
+ */
1765
+ private parseSoundProperty(sound: string | undefined): 'on' | 'off' {
1766
+ if (!sound) {
1767
+ return 'on'; // Default: use audio
1768
+ }
1769
+
1770
+ const trimmed = sound.trim().toLowerCase();
1771
+
1772
+ if (trimmed === 'off') {
1773
+ return 'off';
1774
+ }
1775
+
1776
+ return 'on'; // Default for any other value
1777
+ }
1778
+
1721
1779
  /**
1722
1780
  * Extracts the first <container> child from a fragment element
1723
1781
  */
@@ -1919,7 +1977,7 @@ export class HTMLProjectParser {
1919
1977
  assets: Map<string, Asset>,
1920
1978
  trimLeft: number,
1921
1979
  trimRight: number,
1922
- ): number {
1980
+ ): number | CompiledExpression {
1923
1981
  if (!duration || duration.trim() === 'auto') {
1924
1982
  // Auto: use asset duration minus trim-start and trim-end
1925
1983
  const asset = assets.get(assetName);
@@ -1929,9 +1987,16 @@ export class HTMLProjectParser {
1929
1987
  return Math.max(0, asset.duration - trimLeft - trimRight);
1930
1988
  }
1931
1989
 
1990
+ const trimmed = duration.trim();
1991
+
1992
+ // Check if it's a calc() expression
1993
+ if (trimmed.startsWith('calc(')) {
1994
+ return parseValueLazy(trimmed) as CompiledExpression;
1995
+ }
1996
+
1932
1997
  // Handle percentage (e.g., "100%", "50%")
1933
- if (duration.endsWith('%')) {
1934
- const percentage = parseFloat(duration);
1998
+ if (trimmed.endsWith('%')) {
1999
+ const percentage = parseFloat(trimmed);
1935
2000
  if (isNaN(percentage)) {
1936
2001
  return 0;
1937
2002
  }
@@ -1946,12 +2011,12 @@ export class HTMLProjectParser {
1946
2011
  }
1947
2012
 
1948
2013
  // Handle time value (e.g., "5000ms", "5s")
1949
- return this.parseMilliseconds(duration);
2014
+ return this.parseMilliseconds(trimmed);
1950
2015
  }
1951
2016
 
1952
2017
  /**
1953
2018
  * Parses time value into milliseconds
1954
- * Supports: "5s", "5000ms", "1.5s", etc.
2019
+ * Supports: "5s", "5000ms", "1.5s", or raw numbers (defaults to milliseconds)
1955
2020
  */
1956
2021
  private parseMilliseconds(value: string | undefined): number {
1957
2022
  if (!value) {
@@ -1976,6 +2041,12 @@ export class HTMLProjectParser {
1976
2041
  }
1977
2042
  }
1978
2043
 
2044
+ // Handle raw numbers (default to milliseconds)
2045
+ const num = parseFloat(trimmed);
2046
+ if (!isNaN(num)) {
2047
+ return Math.round(num);
2048
+ }
2049
+
1979
2050
  return 0;
1980
2051
  }
1981
2052
 
@@ -2023,6 +2094,81 @@ export class HTMLProjectParser {
2023
2094
  return this.parseMilliseconds(trimmed);
2024
2095
  }
2025
2096
 
2097
+ /**
2098
+ * Parses data-timing attribute with short syntax
2099
+ * Format: "ts=3000,te=5000,d=2000,os=1000,oe=7000"
2100
+ * Where:
2101
+ * ts = -trim-start
2102
+ * te = -trim-end
2103
+ * d = -duration
2104
+ * os = -offset-start
2105
+ * oe = -offset-end
2106
+ * Values default to milliseconds if no unit specified
2107
+ * Supports calc() expressions: ts=calc(url(#id.time.start))
2108
+ */
2109
+ private parseDataTiming(dataTiming: string | undefined): {
2110
+ trimStart?: number;
2111
+ trimEnd?: number;
2112
+ duration?: number | CompiledExpression;
2113
+ offsetStart?: number | CompiledExpression;
2114
+ offsetEnd?: number | CompiledExpression;
2115
+ } {
2116
+ const result: {
2117
+ trimStart?: number;
2118
+ trimEnd?: number;
2119
+ duration?: number | CompiledExpression;
2120
+ offsetStart?: number | CompiledExpression;
2121
+ offsetEnd?: number | CompiledExpression;
2122
+ } = {};
2123
+
2124
+ if (!dataTiming) {
2125
+ return result;
2126
+ }
2127
+
2128
+ // Split by comma and parse each key-value pair
2129
+ const pairs = dataTiming.split(',').map((pair) => pair.trim());
2130
+
2131
+ for (const pair of pairs) {
2132
+ const [key, value] = pair.split('=').map((s) => s.trim());
2133
+ if (!key || !value) {
2134
+ continue;
2135
+ }
2136
+
2137
+ // Parse value - check if it's a calc() expression or a simple value
2138
+ let parsedValue: number | CompiledExpression;
2139
+
2140
+ if (value.startsWith('calc(')) {
2141
+ // It's a calc() expression
2142
+ parsedValue = parseValueLazy(value) as CompiledExpression;
2143
+ } else {
2144
+ // It's a simple time value - parse with parseMilliseconds
2145
+ // which handles units like 's' and 'ms', defaulting to ms
2146
+ parsedValue = this.parseMilliseconds(value);
2147
+ }
2148
+
2149
+ // Map short names to result properties
2150
+ switch (key) {
2151
+ case 'ts':
2152
+ result.trimStart = parsedValue as number;
2153
+ break;
2154
+ case 'te':
2155
+ result.trimEnd = parsedValue as number;
2156
+ break;
2157
+ case 'd':
2158
+ result.duration = parsedValue;
2159
+ break;
2160
+ case 'os':
2161
+ result.offsetStart = parsedValue;
2162
+ break;
2163
+ case 'oe':
2164
+ result.offsetEnd = parsedValue;
2165
+ break;
2166
+ }
2167
+ }
2168
+
2169
+ return result;
2170
+ }
2171
+
2026
2172
  /**
2027
2173
  * Parses z-index values (-overlay-start-z-index, -overlay-end-z-index)
2028
2174
  */
package/src/project.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  FFmpegOption,
6
6
  Upload,
7
7
  AIProvider,
8
+ SequenceDebugInfo,
8
9
  } from './type';
9
10
  import { Label } from './ffmpeg';
10
11
  import { AssetManager } from './asset-manager';
@@ -13,11 +14,13 @@ import { FilterBuffer } from './stream';
13
14
  import { ExpressionContext, FragmentData } from './expression-parser';
14
15
  import { renderContainers } from './container-renderer';
15
16
  import { renderApps } from './app-renderer';
17
+ import { buildAppsIfNeeded } from './app-builder';
16
18
  import { dirname } from 'path';
17
19
 
18
20
  export class Project {
19
21
  private assetManager: AssetManager;
20
22
  private expressionContext: ExpressionContext;
23
+ private sequencesDebugInfo: SequenceDebugInfo[] = [];
21
24
 
22
25
  constructor(
23
26
  private sequencesDefinitions: SequenceDefinition[],
@@ -46,6 +49,8 @@ export class Project {
46
49
 
47
50
  let buf = new FilterBuffer();
48
51
  let mainSequence: Sequence | null = null;
52
+ this.sequencesDebugInfo = []; // Reset debug info
53
+ let sequenceIndex = 0;
49
54
 
50
55
  this.sequencesDefinitions.forEach((sequenceDefinition) => {
51
56
  const seq = new Sequence(
@@ -61,6 +66,14 @@ export class Project {
61
66
 
62
67
  seq.build();
63
68
 
69
+ // Collect debug info
70
+ this.sequencesDebugInfo.push({
71
+ sequenceIndex,
72
+ totalDuration: seq.getTotalDuration(),
73
+ fragments: seq.getDebugInfo(),
74
+ });
75
+ sequenceIndex++;
76
+
64
77
  if (!mainSequence) {
65
78
  mainSequence = seq;
66
79
  } else {
@@ -95,6 +108,47 @@ export class Project {
95
108
  });
96
109
  }
97
110
 
111
+ public printDebugInfo() {
112
+ console.log('\n=== Debug: Sequences & Fragments Timeline ===\n');
113
+
114
+ if (this.sequencesDebugInfo.length === 0) {
115
+ console.log('No sequences built yet.\n');
116
+ return;
117
+ }
118
+
119
+ this.sequencesDebugInfo.forEach((seqInfo) => {
120
+ console.log(`\nšŸ“¹ Sequence ${seqInfo.sequenceIndex}`);
121
+ console.log(` Total Duration: ${Math.round(seqInfo.totalDuration)}ms`);
122
+ console.log(` Fragments: ${seqInfo.fragments.length}\n`);
123
+
124
+ seqInfo.fragments.forEach((frag, index) => {
125
+ const status = frag.enabled ? 'āœ“' : 'āœ—';
126
+
127
+ // Show ID if it's meaningful (not auto-generated fragment_xxx)
128
+ const isAutoGeneratedId = frag.id.startsWith('fragment_');
129
+ const idDisplay = isAutoGeneratedId ? '' : ` id="${frag.id}"`;
130
+
131
+ console.log(` ${status} [${index + 1}]${idDisplay}`);
132
+ console.log(` Asset: ${frag.assetName}`);
133
+ console.log(` Start: ${Math.round(frag.startTime)}ms`);
134
+ console.log(` End: ${Math.round(frag.endTime)}ms`);
135
+ console.log(` Duration: ${Math.round(frag.duration)}ms`);
136
+
137
+ // Only show non-zero values
138
+ if (Math.round(frag.trimLeft) > 0) {
139
+ console.log(` Trim Left: ${Math.round(frag.trimLeft)}ms`);
140
+ }
141
+ if (Math.round(frag.overlayLeft) > 0) {
142
+ console.log(` Overlay: ${Math.round(frag.overlayLeft)}ms`);
143
+ }
144
+
145
+ console.log('');
146
+ });
147
+ });
148
+
149
+ console.log('===========================================\n');
150
+ }
151
+
98
152
  public getAssetManager(): AssetManager {
99
153
  return this.assetManager;
100
154
  }
@@ -222,10 +276,12 @@ export class Project {
222
276
  * Renders all apps and creates virtual assets for them.
223
277
  * Apps must dispatch "sts-render-complete" on document when ready,
224
278
  * or rendering will fail after a 5-second timeout.
279
+ * @param forceAppBuild - If true, rebuilds apps even if output exists
225
280
  */
226
281
  public async renderApps(
227
282
  outputName: string,
228
283
  activeCacheKeys?: Set<string>,
284
+ forceAppBuild: boolean = false,
229
285
  ): Promise<void> {
230
286
  const output = this.getOutput(outputName);
231
287
  if (!output) {
@@ -245,6 +301,12 @@ export class Project {
245
301
  const apps = fragmentsWithApps.map((frag) => frag.app!);
246
302
  const projectDir = dirname(this.projectPath);
247
303
 
304
+ // Build apps if needed (checks for dst/dist directories with package.json)
305
+ // Deduplicate app sources to avoid building the same app multiple times
306
+ const appSources = apps.map((app) => app.src);
307
+ const uniqueAppSources = [...new Set(appSources)];
308
+ await buildAppsIfNeeded(uniqueAppSources, projectDir, forceAppBuild);
309
+
248
310
  const results = await renderApps(
249
311
  apps,
250
312
  output.resolution.width,
@@ -38,6 +38,7 @@ export const getSampleSequences = (
38
38
  objectFitContainPillarboxColor: '#000000',
39
39
  chromakey: false,
40
40
  chromakeyBlend: 0.1,
41
+ sound: 'on' as const,
41
42
  chromakeySimilarity: 0.1,
42
43
  chromakeyColor: '#000000',
43
44
  },
@@ -61,6 +62,7 @@ export const getSampleSequences = (
61
62
  objectFitContainPillarboxColor: '#000000',
62
63
  chromakey: false,
63
64
  chromakeyBlend: 0.1,
65
+ sound: 'on' as const,
64
66
  chromakeySimilarity: 0.1,
65
67
  chromakeyColor: '#000000',
66
68
  },
@@ -86,6 +88,7 @@ export const getSampleSequences = (
86
88
  chromakeyBlend: 0.1,
87
89
  chromakeySimilarity: 0.1,
88
90
  chromakeyColor: '#000000',
91
+ sound: 'on' as const,
89
92
  },
90
93
  {
91
94
  id: 'f_04',
@@ -107,6 +110,7 @@ export const getSampleSequences = (
107
110
  objectFitContainPillarboxColor: '#000000',
108
111
  chromakey: false,
109
112
  chromakeyBlend: 0.1,
113
+ sound: 'on' as const,
110
114
  chromakeySimilarity: 0.1,
111
115
  chromakeyColor: '#000000',
112
116
  },
@@ -130,6 +134,7 @@ export const getSampleSequences = (
130
134
  objectFitContainPillarboxColor: '#000000',
131
135
  chromakey: false,
132
136
  chromakeyBlend: 0.1,
137
+ sound: 'on' as const,
133
138
  chromakeySimilarity: 0.1,
134
139
  chromakeyColor: '#000000',
135
140
  },
@@ -165,6 +170,7 @@ export const getSampleSequences = (
165
170
  objectFitContainPillarboxColor: '#000000',
166
171
  chromakey: false,
167
172
  chromakeyBlend: 0.1,
173
+ sound: 'on' as const,
168
174
  chromakeySimilarity: 0.1,
169
175
  chromakeyColor: '#000000',
170
176
  },
@@ -200,6 +206,7 @@ export const getSampleSequences = (
200
206
  objectFitContainPillarboxColor: '#000000',
201
207
  chromakey: false,
202
208
  chromakeyBlend: 0.1,
209
+ sound: 'on' as const,
203
210
  chromakeySimilarity: 0.1,
204
211
  chromakeyColor: '#000000',
205
212
  },