codeplay-common 4.4.0 → 4.4.1
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/.gitattributes +2 -2
- package/LICENSE +21 -21
- package/README.md +125 -125
- package/files/buildCodeplay/codeplayBeforeBuild-6.2.js +4107 -4107
- package/files/buildCodeplay/versions.json +23 -23
- package/files/finalrelease +51 -51
- package/files/generate-ai-promo-video.js +140 -15
- package/files/iap-install-2.js +145 -145
- package/files/ionic.config.json +6 -6
- package/files/scripts/setup-baseline-profile.js +262 -262
- package/files/take-screen-video.js +2 -0
- package/package.json +15 -15
- package/scripts/sync-files.js +103 -103
- package/scripts/uninstall.js +109 -109
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"plugins":[
|
|
3
|
-
{"name":"backbutton","pattern":"backbutton-(\\d+\\.\\d+)\\.js$","minVersion":"2.1","baseDir":"js","mandatoryUpdate":true},
|
|
1
|
+
{
|
|
2
|
+
"plugins":[
|
|
3
|
+
{"name":"backbutton","pattern":"backbutton-(\\d+\\.\\d+)\\.js$","minVersion":"2.1","baseDir":"js","mandatoryUpdate":true},
|
|
4
4
|
{"name":"common-js","pattern":"common-(\\d+\\.\\d+)(?:-beta-(\\d+))?\\.js$","minVersion":"6.7","baseDir":"js","mandatoryUpdate":true},
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
{"name":"admob-emi-nextgen","pattern":"Ads[\\\\/]admob-emi-nextgen-(\\d+\\.\\d+)\\.js$","minVersion":"1.0","baseDir":"js","mandatoryUpdate":true},
|
|
7
|
-
{"name":"video-player","pattern":"video-player-(\\d+\\.\\d+)\\.js$","minVersion":"1.6","baseDir":"js","mandatoryUpdate":true},
|
|
8
|
-
{"name":"image-cropper","pattern":"image-cropper-(\\d+\\.\\d+)\\.js$","minVersion":"1.1","baseDir":"js","mandatoryUpdate":true},
|
|
9
|
-
{"name":"beautify","pattern":"beautify-(\\d+\\.\\d+)\\.js$","minVersion":"1.1","baseDir":"js","mandatoryUpdate":true},
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{"name":"localization","pattern":"localization-(\\d+\\.\\d+)$","minVersion":"1.8","baseDir":"js/localization","mandatoryUpdate":true,"isFolder":true},
|
|
13
|
-
{"name": "localization_settings","pattern": "localization_settings-(\\d+\\.\\d+)\\.js$","minVersion": "1.1","required": true,"baseDir": "js/localization","mandatoryUpdate": true},
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{"name":"editor","pattern":"editor-(\\d+\\.\\d+)$","minVersion":"2.0","baseDir":"js/editor","mandatoryUpdate":true,"isFolder":true},
|
|
17
|
-
{"name":"ffmpeg","pattern":"ffmpeg-(\\d+\\.\\d+)$","minVersion":"1.6","baseDir":"js","mandatoryUpdate":true,"isFolder":true},
|
|
18
|
-
{"name":"theme","pattern":"theme-(\\d+\\.\\d+)$","minVersion":"3.5","baseDir":"theme","destDir":"theme","mandatoryUpdate":true,"isFolder":true},
|
|
19
|
-
{"name":"certificatejs","pattern":"certificatejs-(\\d+\\.\\d+)$","minVersion":"1.9","baseDir":"certificate","destDir":"certificate","mandatoryUpdate":true,"isFolder":true},
|
|
7
|
+
{"name":"video-player","pattern":"video-player-(\\d+\\.\\d+)\\.js$","minVersion":"1.6","baseDir":"js","mandatoryUpdate":true},
|
|
8
|
+
{"name":"image-cropper","pattern":"image-cropper-(\\d+\\.\\d+)\\.js$","minVersion":"1.1","baseDir":"js","mandatoryUpdate":true},
|
|
9
|
+
{"name":"beautify","pattern":"beautify-(\\d+\\.\\d+)\\.js$","minVersion":"1.1","baseDir":"js","mandatoryUpdate":true},
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
{"name":"localization","pattern":"localization-(\\d+\\.\\d+)$","minVersion":"1.8","baseDir":"js/localization","mandatoryUpdate":true,"isFolder":true},
|
|
13
|
+
{"name": "localization_settings","pattern": "localization_settings-(\\d+\\.\\d+)\\.js$","minVersion": "1.1","required": true,"baseDir": "js/localization","mandatoryUpdate": true},
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
{"name":"editor","pattern":"editor-(\\d+\\.\\d+)$","minVersion":"2.0","baseDir":"js/editor","mandatoryUpdate":true,"isFolder":true},
|
|
17
|
+
{"name":"ffmpeg","pattern":"ffmpeg-(\\d+\\.\\d+)$","minVersion":"1.6","baseDir":"js","mandatoryUpdate":true,"isFolder":true},
|
|
18
|
+
{"name":"theme","pattern":"theme-(\\d+\\.\\d+)$","minVersion":"3.5","baseDir":"theme","destDir":"theme","mandatoryUpdate":true,"isFolder":true},
|
|
19
|
+
{"name":"certificatejs","pattern":"certificatejs-(\\d+\\.\\d+)$","minVersion":"1.9","baseDir":"certificate","destDir":"certificate","mandatoryUpdate":true,"isFolder":true},
|
|
20
20
|
{"name":"IAP","pattern":"IAP-(\\d+\\.\\d+)$","minVersion":"3.6","baseDir":"js/Ads","mandatoryUpdate":true,"isFolder":true},
|
|
21
|
-
{"name":"common-less","pattern":"common-(\\d+\\.\\d+)\\.less$","minVersion":"1.8","baseDir":"assets/css","mandatoryUpdate":true},
|
|
22
|
-
{"name":"localNotification_AppSettings","pattern":"localNotification_AppSettings-(\\d+\\.\\d+)\\.js$","minVersion":"1.0","baseDir":"js","mandatoryUpdate":true},
|
|
23
|
-
{"name":"localNotification","pattern":"localNotification-(\\d+\\.\\d+)\\.js$","minVersion":"2.2","baseDir":"js","mandatoryUpdate":true},
|
|
24
|
-
{"name":"onesignal","pattern":"onesignal-(\\d+\\.\\d+)\\.js$","minVersion":"2.4","baseDir":"js","mandatoryUpdate":true},
|
|
25
|
-
{"name":"saveToGalleryAndSaveAnyFile","pattern":"saveToGalleryAndSaveAnyFile-(\\d+\\.\\d+)(-ios)?\\.js$","minVersion":"3.1","baseDir":"js","mandatoryUpdate":true}
|
|
26
|
-
]
|
|
21
|
+
{"name":"common-less","pattern":"common-(\\d+\\.\\d+)\\.less$","minVersion":"1.8","baseDir":"assets/css","mandatoryUpdate":true},
|
|
22
|
+
{"name":"localNotification_AppSettings","pattern":"localNotification_AppSettings-(\\d+\\.\\d+)\\.js$","minVersion":"1.0","baseDir":"js","mandatoryUpdate":true},
|
|
23
|
+
{"name":"localNotification","pattern":"localNotification-(\\d+\\.\\d+)\\.js$","minVersion":"2.2","baseDir":"js","mandatoryUpdate":true},
|
|
24
|
+
{"name":"onesignal","pattern":"onesignal-(\\d+\\.\\d+)\\.js$","minVersion":"2.4","baseDir":"js","mandatoryUpdate":true},
|
|
25
|
+
{"name":"saveToGalleryAndSaveAnyFile","pattern":"saveToGalleryAndSaveAnyFile-(\\d+\\.\\d+)(-ios)?\\.js$","minVersion":"3.1","baseDir":"js","mandatoryUpdate":true}
|
|
26
|
+
]
|
|
27
27
|
}
|
package/files/finalrelease
CHANGED
|
@@ -275,14 +275,14 @@ const checkIsAdsDisableByReturnStatement=()=>{
|
|
|
275
275
|
|
|
276
276
|
|
|
277
277
|
const adsFolder = join('src', 'js', 'Ads');
|
|
278
|
-
const filePattern = /^admob-emi-nextgen-(\d+\.)+\d+\.js$/;
|
|
278
|
+
const filePattern = /^admob-emi-nextgen-(\d+\.)+\d+\.js$/;
|
|
279
279
|
|
|
280
280
|
// Step 1: Find the admob file
|
|
281
281
|
const files = readdirSync(adsFolder);
|
|
282
282
|
const admobFile = files.find(f => filePattern.test(f));
|
|
283
283
|
|
|
284
284
|
if (!admobFile) {
|
|
285
|
-
console.log('❌ No Admob nextgen file found.');
|
|
285
|
+
console.log('❌ No Admob nextgen file found.');
|
|
286
286
|
process.exit(1);
|
|
287
287
|
}
|
|
288
288
|
|
|
@@ -527,30 +527,30 @@ if (existsSync(aabOutputDir)) {
|
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
// Extract version code and version name from build.gradle
|
|
530
|
-
const gradleFilePath = join("android", "app", "build.gradle");
|
|
531
|
-
const gradleContent = readFileSync(gradleFilePath, 'utf8');
|
|
532
|
-
|
|
533
|
-
// Return the character range of a Gradle block, allowing nested blocks inside it.
|
|
534
|
-
function findGradleBlock(content, headerPattern) {
|
|
535
|
-
const headerMatch = content.match(headerPattern);
|
|
536
|
-
if (!headerMatch) return null;
|
|
537
|
-
const openingBrace = content.indexOf('{', headerMatch.index + headerMatch[0].length);
|
|
538
|
-
if (openingBrace === -1) return null;
|
|
539
|
-
let depth = 0;
|
|
540
|
-
let quote = null;
|
|
541
|
-
for (let index = openingBrace; index < content.length; index += 1) {
|
|
542
|
-
const character = content[index];
|
|
543
|
-
const previous = content[index - 1];
|
|
544
|
-
if (quote) {
|
|
545
|
-
if (character === quote && previous !== '\\') quote = null;
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
if (character === '\"' || character === "'") quote = character;
|
|
549
|
-
else if (character === '{') depth += 1;
|
|
550
|
-
else if (character === '}' && --depth === 0) return { start: openingBrace + 1, end: index };
|
|
551
|
-
}
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
530
|
+
const gradleFilePath = join("android", "app", "build.gradle");
|
|
531
|
+
const gradleContent = readFileSync(gradleFilePath, 'utf8');
|
|
532
|
+
|
|
533
|
+
// Return the character range of a Gradle block, allowing nested blocks inside it.
|
|
534
|
+
function findGradleBlock(content, headerPattern) {
|
|
535
|
+
const headerMatch = content.match(headerPattern);
|
|
536
|
+
if (!headerMatch) return null;
|
|
537
|
+
const openingBrace = content.indexOf('{', headerMatch.index + headerMatch[0].length);
|
|
538
|
+
if (openingBrace === -1) return null;
|
|
539
|
+
let depth = 0;
|
|
540
|
+
let quote = null;
|
|
541
|
+
for (let index = openingBrace; index < content.length; index += 1) {
|
|
542
|
+
const character = content[index];
|
|
543
|
+
const previous = content[index - 1];
|
|
544
|
+
if (quote) {
|
|
545
|
+
if (character === quote && previous !== '\\') quote = null;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (character === '\"' || character === "'") quote = character;
|
|
549
|
+
else if (character === '{') depth += 1;
|
|
550
|
+
else if (character === '}' && --depth === 0) return { start: openingBrace + 1, end: index };
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
554
|
|
|
555
555
|
const versionCodeMatch = gradleContent.match(/versionCode\s+(\d+)/);
|
|
556
556
|
const versionNameMatch = gradleContent.match(/versionName\s+"([^"]+)"/);
|
|
@@ -617,26 +617,26 @@ rl.question('Enter new versionCode (press enter to keep current): ', (newVersion
|
|
|
617
617
|
`$1\n minifyEnabled ${desiredMinify}`
|
|
618
618
|
);
|
|
619
619
|
console.log(`✅ Inserted minifyEnabled ${desiredMinify} into release block.`);
|
|
620
|
-
} else {
|
|
621
|
-
console.log('⚠️ Warning: buildTypes > release block not found. minifyEnabled was not added.');
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Add native debug symbols for release AABs, but leave any existing setting untouched.
|
|
625
|
-
const releaseBlock = findGradleBlock(updatedGradleContent, /buildTypes\s*\{[\s\S]*?\brelease\s*/);
|
|
626
|
-
if (releaseBlock) {
|
|
627
|
-
const releaseContent = updatedGradleContent.slice(releaseBlock.start, releaseBlock.end);
|
|
628
|
-
if (/\bdebugSymbolLevel\b/.test(releaseContent)) {
|
|
629
|
-
console.log('ℹ️ debugSymbolLevel already exists in buildTypes.release; leaving it unchanged.');
|
|
630
|
-
} else {
|
|
631
|
-
const ndkBlock = `\n ndk {\n debugSymbolLevel 'FULL'\n }`;
|
|
632
|
-
updatedGradleContent = updatedGradleContent.slice(0, releaseBlock.end)
|
|
633
|
-
+ ndkBlock
|
|
634
|
-
+ updatedGradleContent.slice(releaseBlock.end);
|
|
635
|
-
console.log("✅ Added debugSymbolLevel 'FULL' to buildTypes.release.");
|
|
636
|
-
}
|
|
637
|
-
} else {
|
|
638
|
-
console.log('⚠️ Warning: buildTypes.release block not found. debugSymbolLevel was not added.');
|
|
639
|
-
}
|
|
620
|
+
} else {
|
|
621
|
+
console.log('⚠️ Warning: buildTypes > release block not found. minifyEnabled was not added.');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Add native debug symbols for release AABs, but leave any existing setting untouched.
|
|
625
|
+
const releaseBlock = findGradleBlock(updatedGradleContent, /buildTypes\s*\{[\s\S]*?\brelease\s*/);
|
|
626
|
+
if (releaseBlock) {
|
|
627
|
+
const releaseContent = updatedGradleContent.slice(releaseBlock.start, releaseBlock.end);
|
|
628
|
+
if (/\bdebugSymbolLevel\b/.test(releaseContent)) {
|
|
629
|
+
console.log('ℹ️ debugSymbolLevel already exists in buildTypes.release; leaving it unchanged.');
|
|
630
|
+
} else {
|
|
631
|
+
const ndkBlock = `\n ndk {\n debugSymbolLevel 'FULL'\n }`;
|
|
632
|
+
updatedGradleContent = updatedGradleContent.slice(0, releaseBlock.end)
|
|
633
|
+
+ ndkBlock
|
|
634
|
+
+ updatedGradleContent.slice(releaseBlock.end);
|
|
635
|
+
console.log("✅ Added debugSymbolLevel 'FULL' to buildTypes.release.");
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
console.log('⚠️ Warning: buildTypes.release block not found. debugSymbolLevel was not added.');
|
|
639
|
+
}
|
|
640
640
|
|
|
641
641
|
|
|
642
642
|
|
|
@@ -753,11 +753,11 @@ rl.question('Enter new versionCode (press enter to keep current): ', (newVersion
|
|
|
753
753
|
|
|
754
754
|
|
|
755
755
|
// Copy the built files to the appropriate folder
|
|
756
|
-
const src = join("www");
|
|
757
|
-
const dest = join("android", "app", "src", "main", "assets", "public");
|
|
758
|
-
|
|
759
|
-
// Copy the web build with Node so releases work on every platform.
|
|
760
|
-
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
756
|
+
const src = join("www");
|
|
757
|
+
const dest = join("android", "app", "src", "main", "assets", "public");
|
|
758
|
+
|
|
759
|
+
// Copy the web build with Node so releases work on every platform.
|
|
760
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
761
761
|
|
|
762
762
|
// Build Android AAB file
|
|
763
763
|
//child_process.execSync('cd android && ./gradlew bundleRelease', { stdio: 'inherit' });
|
|
@@ -13,6 +13,15 @@ const capacitorConfig = JSON.parse(fs.readFileSync(capacitorConfigPath, 'utf8'))
|
|
|
13
13
|
const appName = String(capacitorConfig.appName || '').trim();
|
|
14
14
|
const appUniqueId = String(capacitorConfig.android?.APP_UNIQUE_ID ?? '').trim();
|
|
15
15
|
const packageId = String(capacitorConfig.appId || '').trim();
|
|
16
|
+
const iosStoreConfigPath = path.join(projectDirectory, 'Auto-AI-Video', 'ios-store-config.json');
|
|
17
|
+
let iosStoreConfig = {};
|
|
18
|
+
if (fs.existsSync(iosStoreConfigPath)) {
|
|
19
|
+
try {
|
|
20
|
+
iosStoreConfig = JSON.parse(fs.readFileSync(iosStoreConfigPath, 'utf8'));
|
|
21
|
+
} catch {
|
|
22
|
+
iosStoreConfig = {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
if (!appName) throw new Error('capacitor.config.json is missing appName.');
|
|
18
27
|
if (!appUniqueId) throw new Error('capacitor.config.json is missing android.APP_UNIQUE_ID.');
|
|
@@ -20,6 +29,21 @@ if (!packageId) throw new Error('capacitor.config.json is missing appId.');
|
|
|
20
29
|
|
|
21
30
|
const appFilePrefix = `${appUniqueId}. ${sanitizeFileNamePart(appName)}`;
|
|
22
31
|
const playStoreDownloadUrl = `https://play.google.com/store/apps/details?id=${encodeURIComponent(packageId)}`;
|
|
32
|
+
const configuredAppStoreUrl = String(iosStoreConfig.appStoreUrl || '')
|
|
33
|
+
.match(/https:\/\/apps\.apple\.com\/app\/id\d+/)?.[0] || '';
|
|
34
|
+
let appStoreDownloadUrl = /^https:\/\/apps\.apple\.com\/app\/id\d+$/.test(configuredAppStoreUrl)
|
|
35
|
+
? configuredAppStoreUrl
|
|
36
|
+
: '';
|
|
37
|
+
const promoDesignIndex = createHash('sha1').update(`${appUniqueId}:${appName}`).digest()[0] % 4;
|
|
38
|
+
const promoDesigns = [
|
|
39
|
+
{ accent: '0x23d5c3', background: '0x07101f', panelX: 105, panelWidth: 500, phoneX: 134 },
|
|
40
|
+
{ accent: '0xffb020', background: '0x241006', panelX: 1290, panelWidth: 500, phoneX: 64 },
|
|
41
|
+
{ accent: '0x9b7cff', background: '0x160d2b', panelX: 105, panelWidth: 620, phoneX: 134 },
|
|
42
|
+
{ accent: '0xff5b8d', background: '0x21101a', panelX: 1240, panelWidth: 550, phoneX: 64 },
|
|
43
|
+
];
|
|
44
|
+
const promoDesign = promoDesigns[promoDesignIndex];
|
|
45
|
+
let endCardStart = 0;
|
|
46
|
+
const endCardDuration = 8;
|
|
23
47
|
const sourceVideoDirectory = path.join(projectDirectory, 'Auto-Screenshot', 'Video', 'Output');
|
|
24
48
|
const outputVideoDirectory = path.join(projectDirectory, 'Auto-AI-Video');
|
|
25
49
|
const spatialXrDirectory = path.join(outputVideoDirectory, 'Google-Play-XR-Spatial');
|
|
@@ -38,6 +62,7 @@ const promoCopyCachePath = path.join(
|
|
|
38
62
|
`${appFilePrefix}-promo-copy.json`,
|
|
39
63
|
);
|
|
40
64
|
const analysisFrameDirectory = path.join(temporaryDirectory, 'ai-analysis');
|
|
65
|
+
const endCardBannerPath = path.join(outputVideoDirectory, `${appFilePrefix}-ai-end-card-banner.png`);
|
|
41
66
|
const promoCopySchemaPath = path.join(temporaryDirectory, 'promo-copy-schema.json');
|
|
42
67
|
const promoCopyResultPath = path.join(temporaryDirectory, 'promo-copy-result.json');
|
|
43
68
|
const ffmpegFallbackDirectory = 'W:\\Tools\\ffmpeg\\bin';
|
|
@@ -145,6 +170,40 @@ function sanitizeFileNamePart(value) {
|
|
|
145
170
|
.replace(/[. ]+$/g, '') || 'App';
|
|
146
171
|
}
|
|
147
172
|
|
|
173
|
+
async function askIosStoreDetails() {
|
|
174
|
+
if (iosStoreConfig.configured === true
|
|
175
|
+
&& (!iosStoreConfig.available || /^https:\/\/apps\.apple\.com\/app\/id\d+$/.test(appStoreDownloadUrl))) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
179
|
+
throw new Error('iOS availability must be answered in an interactive terminal.');
|
|
180
|
+
}
|
|
181
|
+
const readline = require('node:readline/promises');
|
|
182
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
183
|
+
const appStoreId = (await prompt.question('Enter the numeric Apple App Store ID, or press Enter if unavailable: ')).trim();
|
|
184
|
+
prompt.close();
|
|
185
|
+
const available = Boolean(appStoreId);
|
|
186
|
+
if (available) {
|
|
187
|
+
if (!/^\d+$/.test(appStoreId)) throw new Error('Apple App Store ID must contain only numbers.');
|
|
188
|
+
appStoreDownloadUrl = `https://apps.apple.com/app/id${appStoreId}`;
|
|
189
|
+
}
|
|
190
|
+
fs.mkdirSync(path.dirname(iosStoreConfigPath), { recursive: true });
|
|
191
|
+
fs.writeFileSync(iosStoreConfigPath, `${JSON.stringify({
|
|
192
|
+
configured: true,
|
|
193
|
+
available: available === 'yes',
|
|
194
|
+
appStoreId: available === 'yes' ? appStoreDownloadUrl.split('/').pop().replace(/^id/, '') : '',
|
|
195
|
+
appStoreUrl: appStoreDownloadUrl,
|
|
196
|
+
}, null, 2)}\n`, 'utf8');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function ensureEndCardBanner() {
|
|
200
|
+
if (fs.existsSync(endCardBannerPath)) return;
|
|
201
|
+
throw new Error(
|
|
202
|
+
`AI end-card banner is missing. Generate it manually and save it here:\n${endCardBannerPath}\n\n`
|
|
203
|
+
+ 'Then run the video-generation command again.',
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
148
207
|
function findVideos(directory) {
|
|
149
208
|
if (!fs.existsSync(directory)) return [];
|
|
150
209
|
return fs.readdirSync(directory, { withFileTypes: true })
|
|
@@ -223,6 +282,8 @@ function configurePromoTimeline(sourceDuration) {
|
|
|
223
282
|
maxNarrationWords: Math.max(7, Math.floor(sceneDuration * 2.15)),
|
|
224
283
|
};
|
|
225
284
|
});
|
|
285
|
+
endCardStart = expectedDuration;
|
|
286
|
+
expectedDuration = roundTimelineValue(expectedDuration + endCardDuration);
|
|
226
287
|
|
|
227
288
|
console.log(
|
|
228
289
|
`Adaptive promo timeline: ${expectedDuration}s from ${sourceDuration.toFixed(2)}s source; `
|
|
@@ -394,6 +455,10 @@ function escapeFilterPath(filePath) {
|
|
|
394
455
|
.replace(/'/g, "\\'");
|
|
395
456
|
}
|
|
396
457
|
|
|
458
|
+
function escapeFilterText(value) {
|
|
459
|
+
return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/:/g, '\\\:');
|
|
460
|
+
}
|
|
461
|
+
|
|
397
462
|
function readTextIfPresent(filePath, maximumLength = 12000) {
|
|
398
463
|
if (!fs.existsSync(filePath)) return '';
|
|
399
464
|
return fs.readFileSync(filePath, 'utf8').slice(0, maximumLength);
|
|
@@ -624,7 +689,10 @@ function addPlayStoreDownloadLink(description) {
|
|
|
624
689
|
const hashtags = finalLine.match(/#[\p{L}\p{N}_]+/gu) || [];
|
|
625
690
|
const hasFinalHashtagLine = hashtags.length === 3
|
|
626
691
|
&& !finalLine.replace(/#[\p{L}\p{N}_]+/gu, '').trim();
|
|
627
|
-
const
|
|
692
|
+
const iosBlock = appStoreDownloadUrl
|
|
693
|
+
? `\n\nDownload ${appName} on the App Store:\n${appStoreDownloadUrl}`
|
|
694
|
+
: '';
|
|
695
|
+
const downloadBlock = `Download ${appName} on Google Play:\n${playStoreDownloadUrl}${iosBlock}`;
|
|
628
696
|
|
|
629
697
|
if (!hasFinalHashtagLine) {
|
|
630
698
|
return `${cleanedDescription}\n\n${downloadBlock}`;
|
|
@@ -912,6 +980,7 @@ function writeYoutubeUploadMetadata(promoCopy, sourceVideos) {
|
|
|
912
980
|
appUniqueId,
|
|
913
981
|
packageId,
|
|
914
982
|
playStoreDownloadUrl,
|
|
983
|
+
appStoreDownloadUrl,
|
|
915
984
|
platform,
|
|
916
985
|
videoFile: path.basename(source.outputPath),
|
|
917
986
|
thumbnailFile: path.basename(getThumbnailPath(source.outputPath)),
|
|
@@ -1064,6 +1133,17 @@ function createNarrationSegments(pythonCommand, voiceName, promoCopy) {
|
|
|
1064
1133
|
);
|
|
1065
1134
|
});
|
|
1066
1135
|
|
|
1136
|
+
const endCardPath = path.join(segmentDirectory, 'end-card.mp3');
|
|
1137
|
+
synthesizeNeuralSpeech(
|
|
1138
|
+
pythonCommand,
|
|
1139
|
+
voiceName,
|
|
1140
|
+
appStoreDownloadUrl
|
|
1141
|
+
? 'Now available on Google Play and the App Store. Download links are available in the description.'
|
|
1142
|
+
: 'Now available on Google Play. The download link is available in the description.',
|
|
1143
|
+
endCardPath,
|
|
1144
|
+
);
|
|
1145
|
+
segmentPaths.push(endCardPath);
|
|
1146
|
+
|
|
1067
1147
|
return segmentPaths;
|
|
1068
1148
|
}
|
|
1069
1149
|
|
|
@@ -1086,18 +1166,23 @@ function createNarrationTrack(ffmpegCommand, segmentPaths) {
|
|
|
1086
1166
|
const delay = Math.round((scene.start + (index === 0 ? 0.25 : 0.2)) * 1000);
|
|
1087
1167
|
return `[${index}:a]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,adelay=${delay}|${delay}[n${index}]`;
|
|
1088
1168
|
});
|
|
1169
|
+
const endCardFilter = segmentPaths.length > sceneWindows.length
|
|
1170
|
+
? `[${sceneWindows.length}:a]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,adelay=${endCardStart * 1000}|${endCardStart * 1000},apad=whole_dur=${expectedDuration}[endcard]`
|
|
1171
|
+
: '';
|
|
1089
1172
|
const inputs = sceneWindows.map((scene, index) => `[n${index}]`).join('');
|
|
1090
1173
|
const filterGraph = [
|
|
1091
1174
|
...segmentFilters,
|
|
1092
1175
|
`${inputs}amix=inputs=${sceneWindows.length}:normalize=0:duration=longest,highpass=f=80,lowpass=f=12000,loudnorm=I=-16:TP=-2:LRA=7,apad=whole_dur=${expectedDuration}[narration]`,
|
|
1093
|
-
|
|
1176
|
+
endCardFilter,
|
|
1177
|
+
endCardFilter ? '[narration][endcard]amix=inputs=2:normalize=0:duration=longest,loudnorm=I=-16:TP=-2:LRA=7[narration-final]' : '',
|
|
1178
|
+
].filter(Boolean).join(';');
|
|
1094
1179
|
|
|
1095
1180
|
run(ffmpegCommand, [
|
|
1096
1181
|
'-hide_banner',
|
|
1097
1182
|
'-y',
|
|
1098
1183
|
...inputArguments,
|
|
1099
1184
|
'-filter_complex', filterGraph,
|
|
1100
|
-
'-map', '[narration]',
|
|
1185
|
+
'-map', endCardFilter ? '[narration-final]' : '[narration]',
|
|
1101
1186
|
'-t', String(expectedDuration),
|
|
1102
1187
|
'-c:a', 'pcm_s16le',
|
|
1103
1188
|
'-ar', '48000',
|
|
@@ -1123,12 +1208,9 @@ function getVideoDetails(ffprobeCommand, videoPath) {
|
|
|
1123
1208
|
function validateInput(ffprobeCommand, inputPath) {
|
|
1124
1209
|
const details = getVideoDetails(ffprobeCommand, inputPath);
|
|
1125
1210
|
const videoStream = details.streams?.find((stream) => stream.codec_type === 'video');
|
|
1126
|
-
const audioStream = details.streams?.find((stream) => stream.codec_type === 'audio');
|
|
1127
1211
|
const duration = Number(details.format?.duration || 0);
|
|
1128
1212
|
|
|
1129
|
-
if (!videoStream
|
|
1130
|
-
throw new Error('The source must contain both video and audio streams.');
|
|
1131
|
-
}
|
|
1213
|
+
if (!videoStream) throw new Error('The source must contain a video stream.');
|
|
1132
1214
|
if (Number(videoStream.width) < 480 || Number(videoStream.height) < 1080) {
|
|
1133
1215
|
throw new Error('The source resolution is too small for the promotional layout.');
|
|
1134
1216
|
}
|
|
@@ -1138,6 +1220,40 @@ function validateInput(ffprobeCommand, inputPath) {
|
|
|
1138
1220
|
return details;
|
|
1139
1221
|
}
|
|
1140
1222
|
|
|
1223
|
+
function ensureAudioStream(ffmpegCommand, inputPath, details) {
|
|
1224
|
+
const hasAudioStream = details.streams?.some((stream) => stream.codec_type === 'audio');
|
|
1225
|
+
if (hasAudioStream) return inputPath;
|
|
1226
|
+
|
|
1227
|
+
const sourceHash = createHash('sha1').update(inputPath).digest('hex').slice(0, 12);
|
|
1228
|
+
const audioFallbackPath = path.join(
|
|
1229
|
+
temporaryDirectory,
|
|
1230
|
+
`${sanitizeFileNamePart(path.basename(inputPath, path.extname(inputPath)))}-${sourceHash}-with-silent-audio.mp4`,
|
|
1231
|
+
);
|
|
1232
|
+
if (!fs.existsSync(audioFallbackPath)) {
|
|
1233
|
+
const duration = Number(details.format?.duration || 0);
|
|
1234
|
+
console.log(`Adding a silent audio track to video-only source: ${inputPath}`);
|
|
1235
|
+
fs.mkdirSync(temporaryDirectory, { recursive: true });
|
|
1236
|
+
run(ffmpegCommand, [
|
|
1237
|
+
'-hide_banner',
|
|
1238
|
+
'-y',
|
|
1239
|
+
'-i', inputPath,
|
|
1240
|
+
'-f', 'lavfi',
|
|
1241
|
+
'-t', String(duration),
|
|
1242
|
+
'-i', 'anullsrc=channel_layout=stereo:sample_rate=48000',
|
|
1243
|
+
'-map', '0:v:0',
|
|
1244
|
+
'-map', '1:a:0',
|
|
1245
|
+
'-c:v', 'copy',
|
|
1246
|
+
'-c:a', 'aac',
|
|
1247
|
+
'-b:a', '192k',
|
|
1248
|
+
'-ar', '48000',
|
|
1249
|
+
'-t', String(duration),
|
|
1250
|
+
'-movflags', '+faststart',
|
|
1251
|
+
audioFallbackPath,
|
|
1252
|
+
]);
|
|
1253
|
+
}
|
|
1254
|
+
return audioFallbackPath;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1141
1257
|
function validateOutput(ffprobeCommand, outputPath, targetWidth, targetHeight) {
|
|
1142
1258
|
const details = getVideoDetails(ffprobeCommand, outputPath);
|
|
1143
1259
|
const videoStream = details.streams?.find((stream) => stream.codec_type === 'video') || {};
|
|
@@ -1185,8 +1301,8 @@ function generatePromo(ffmpegCommand, inputPath, outputPath, crop, targetWidth,
|
|
|
1185
1301
|
`[audiosource${index}]atrim=start=${segment.start}:end=${segment.end},asetpts=PTS-STARTPTS[a${index}]`
|
|
1186
1302
|
));
|
|
1187
1303
|
const videoSequenceFilter = segmentCount === 1
|
|
1188
|
-
?
|
|
1189
|
-
: `${editSegments.map((_, index) => `[v${index}]`).join('')}concat=n=${segmentCount}:v=1:a=0,fps=30,setpts=PTS-STARTPTS,split=2[bgsrc][phonesrc]`;
|
|
1304
|
+
? `[v0]fps=30,setpts=PTS-STARTPTS,tpad=stop_mode=clone:stop_duration=${endCardDuration},split=2[bgsrc][phonesrc]`
|
|
1305
|
+
: `${editSegments.map((_, index) => `[v${index}]`).join('')}concat=n=${segmentCount}:v=1:a=0,fps=30,setpts=PTS-STARTPTS,tpad=stop_mode=clone:stop_duration=${endCardDuration},split=2[bgsrc][phonesrc]`;
|
|
1190
1306
|
const audioSequenceFilter = segmentCount === 1
|
|
1191
1307
|
? '[a0]loudnorm=I=-20:TP=-2:LRA=11[appaudio-pre]'
|
|
1192
1308
|
: `${editSegments.map((_, index) => `[a${index}]`).join('')}concat=n=${segmentCount}:v=0:a=1,loudnorm=I=-20:TP=-2:LRA=11[appaudio-pre]`;
|
|
@@ -1196,13 +1312,16 @@ function generatePromo(ffmpegCommand, inputPath, outputPath, crop, targetWidth,
|
|
|
1196
1312
|
videoSourceFilter,
|
|
1197
1313
|
...videoTrimFilters,
|
|
1198
1314
|
videoSequenceFilter,
|
|
1199
|
-
|
|
1200
|
-
|
|
1315
|
+
`[bgsrc]scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,gblur=sigma=34,eq=brightness=-0.40:saturation=1.30,drawbox=x=0:y=0:w=iw:h=ih:color=${promoDesign.background}@0.72:t=fill[background]`,
|
|
1316
|
+
`[background]drawbox=x=${promoDesign.panelX}:y=30:w=${promoDesign.panelWidth}:h=1020:color=black@0.58:t=fill,drawbox=x=${promoDesign.panelX + 7}:y=37:w=${promoDesign.panelWidth - 14}:h=1006:color=${promoDesign.accent}@0.24:t=3,drawbox=x=680:y=176:w=1050:h=2:color=${promoDesign.accent}@0.55:t=fill,drawbox=x=680:y=960:w=1050:h=2:color=white@0.16:t=fill,drawbox=x=680:y=176:w=10:h=786:color=0xffb020@0.90:t=fill[stage]`,
|
|
1201
1317
|
'[phonesrc]scale=-2:980,setsar=1[phone]',
|
|
1202
|
-
|
|
1318
|
+
`[stage][phone]overlay=x=${promoDesign.phoneX}:y=50:shortest=1[layout]`,
|
|
1203
1319
|
'[1:v]scale=145:145,format=rgba,colorchannelmixer=aa=0.96[icon]',
|
|
1204
1320
|
`[layout][icon]overlay=x=720:y=650:enable=between(t\\,${finalSceneStart}\\,${expectedDuration}):shortest=1[branded]`,
|
|
1205
|
-
`[branded]ass=filename='${subtitleFilterPath}',scale=${targetWidth}:${targetHeight}:flags=lanczos,setsar=1,format=yuv420p,setparams=colorspace=bt709:color_primaries=bt709:color_trc=bt709:range=tv[
|
|
1321
|
+
`[branded]ass=filename='${subtitleFilterPath}',scale=${targetWidth}:${targetHeight}:flags=lanczos,setsar=1,format=yuv420p,setparams=colorspace=bt709:color_primaries=bt709:color_trc=bt709:range=tv[captioned]`,
|
|
1322
|
+
`[3:v]scale=1920:1080,format=yuv420p[cardbanner]`,
|
|
1323
|
+
`[captioned][cardbanner]overlay=eof_action=repeat:enable=between(t\\,${endCardStart}\\,${expectedDuration})[cardonly]`,
|
|
1324
|
+
`[cardonly]scale=${targetWidth}:${targetHeight}:flags=lanczos,setsar=1,format=yuv420p[video]`,
|
|
1206
1325
|
audioSourceFilter,
|
|
1207
1326
|
...audioTrimFilters,
|
|
1208
1327
|
audioSequenceFilter,
|
|
@@ -1222,6 +1341,8 @@ function generatePromo(ffmpegCommand, inputPath, outputPath, crop, targetWidth,
|
|
|
1222
1341
|
'-framerate', '30',
|
|
1223
1342
|
'-i', iconPath,
|
|
1224
1343
|
'-i', narrationTrackPath,
|
|
1344
|
+
'-loop', '1',
|
|
1345
|
+
'-i', endCardBannerPath,
|
|
1225
1346
|
'-filter_complex', filterGraph,
|
|
1226
1347
|
'-map', '[video]',
|
|
1227
1348
|
'-map', '[audio]',
|
|
@@ -1372,6 +1493,9 @@ async function main() {
|
|
|
1372
1493
|
return;
|
|
1373
1494
|
}
|
|
1374
1495
|
|
|
1496
|
+
await askIosStoreDetails();
|
|
1497
|
+
await ensureEndCardBanner();
|
|
1498
|
+
|
|
1375
1499
|
const ffmpegCommand = getExecutable('ffmpeg');
|
|
1376
1500
|
const ffprobeCommand = getExecutable('ffprobe');
|
|
1377
1501
|
const voice = resolveVoice(options.voice);
|
|
@@ -1394,6 +1518,7 @@ async function main() {
|
|
|
1394
1518
|
console.log(`Preparing ${inputPaths.length} source video${inputPaths.length === 1 ? '' : 's'}...`);
|
|
1395
1519
|
const sourceVideos = inputPaths.map((inputPath) => {
|
|
1396
1520
|
const details = validateInput(ffprobeCommand, inputPath);
|
|
1521
|
+
const processingInputPath = ensureAudioStream(ffmpegCommand, inputPath, details);
|
|
1397
1522
|
const duration = Number(details.format?.duration || 0);
|
|
1398
1523
|
const crop = detectActiveVideoCrop(ffmpegCommand, inputPath, details);
|
|
1399
1524
|
const target = getTargetDimensions(details);
|
|
@@ -1402,7 +1527,7 @@ async function main() {
|
|
|
1402
1527
|
: options.input
|
|
1403
1528
|
? getSingleOutputPath(inputPath, target.width, target.height)
|
|
1404
1529
|
: getBatchOutputPath(inputPath, target.width, target.height);
|
|
1405
|
-
return { crop, duration, inputPath, outputPath, target };
|
|
1530
|
+
return { crop, duration, inputPath, outputPath, processingInputPath, target };
|
|
1406
1531
|
});
|
|
1407
1532
|
configurePromoTimeline(Math.min(...sourceVideos.map((source) => source.duration)));
|
|
1408
1533
|
const analysisSource = [...sourceVideos].sort((left, right) => (
|
|
@@ -1492,7 +1617,7 @@ async function main() {
|
|
|
1492
1617
|
);
|
|
1493
1618
|
generatePromo(
|
|
1494
1619
|
ffmpegCommand,
|
|
1495
|
-
source.
|
|
1620
|
+
source.processingInputPath,
|
|
1496
1621
|
source.outputPath,
|
|
1497
1622
|
source.crop,
|
|
1498
1623
|
source.target.width,
|