@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.
- package/dist/app-builder.d.ts +18 -0
- package/dist/app-builder.d.ts.map +1 -0
- package/dist/app-builder.js +94 -0
- package/dist/app-builder.js.map +1 -0
- package/dist/cli/commands/filters.d.ts +3 -0
- package/dist/cli/commands/filters.d.ts.map +1 -0
- package/dist/cli/commands/filters.js +21 -0
- package/dist/cli/commands/filters.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +6 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/instagram/instagram-upload-strategy.d.ts +5 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -1
- package/dist/cli/instagram/instagram-upload-strategy.js +46 -3
- package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/html-project-parser.d.ts +20 -1
- package/dist/html-project-parser.d.ts.map +1 -1
- package/dist/html-project-parser.js +128 -15
- package/dist/html-project-parser.js.map +1 -1
- package/dist/project.d.ts +4 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +50 -1
- package/dist/project.js.map +1 -1
- package/dist/sample-sequences.d.ts.map +1 -1
- package/dist/sample-sequences.js +7 -0
- package/dist/sample-sequences.js.map +1 -1
- package/dist/sequence.d.ts +4 -1
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +43 -19
- package/dist/sequence.js.map +1 -1
- package/dist/type.d.ts +18 -1
- package/dist/type.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/app-builder.ts +113 -0
- package/src/cli/commands/filters.ts +21 -0
- package/src/cli/commands/generate.ts +10 -1
- package/src/cli/instagram/instagram-upload-strategy.ts +61 -1
- package/src/cli.ts +2 -0
- package/src/html-project-parser.ts +172 -26
- package/src/project.ts +62 -0
- package/src/sample-sequences.ts +7 -0
- package/src/sequence.ts +50 -20
- 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
|
-
//
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
//
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
//
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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.
|
|
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 (
|
|
1934
|
-
const percentage = parseFloat(
|
|
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(
|
|
2014
|
+
return this.parseMilliseconds(trimmed);
|
|
1950
2015
|
}
|
|
1951
2016
|
|
|
1952
2017
|
/**
|
|
1953
2018
|
* Parses time value into milliseconds
|
|
1954
|
-
* Supports: "5s", "5000ms", "1.5s",
|
|
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,
|
package/src/sample-sequences.ts
CHANGED
|
@@ -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
|
},
|