1<!doctype html> 2<html lang="en"> 3 4<head> 5 <meta charset="UTF-8" /> 6 <meta name="google-site-verification" content="Zl_QQ11HjpZNZRX5Zg1fZoAVTGwjAXX-oUYNdTLKE54" /> 7 <meta data-react-helmet="true" name="description" 8 content="Create faceless AI videos and digital products. Produce faceless YouTube videos with AI voiceovers and scenes, and build ebooks, planners, spreadsheets and images. Digital Maker AI is the all-in-one platform for creators." /> 9 <meta data-react-helmet="true" name="keywords" 10 content="faceless video maker, faceless YouTube videos, AI video generator, faceless AI videos, digital product maker, AI digital products, create digital products, ebook maker, AI spreadsheet, digital product creator, online business tools" /> 11 <meta data-react-helmet="true" property="og:title" content="Digital Maker AI - Create Faceless AI Videos & Digital Products" /> 12 <meta data-react-helmet="true" property="og:description" 13 content="Create faceless AI videos and digital products in minutes, not months. Produce faceless YouTube videos with AI, and generate ebooks, planners, images and websites." /> 14 <meta data-react-helmet="true" property="og:type" content="website" /> 15 <meta data-react-helmet="true" property="og:url" content="https://digitalmaker.ai" /> 16 <meta data-react-helmet="true" property="og:image" 17 content="https://auth.digitalmaker.ai/storage/v1/object/public/digital-maker-outputs/favicon-256.png" /> 18 <meta data-react-helmet="true" name="twitter:card" content="summary_large_image" /> 19 <meta data-react-helmet="true" name="twitter:title" content="Digital Maker AI - Create Faceless AI Videos & Digital Products" /> 20 <meta data-react-helmet="true" name="twitter:description" 21 content="Create faceless AI videos and digital products in minutes, not months. AI tools for faceless YouTube video, ebooks, planners, images and websites." /> 22 <!-- Primary tab favicon embedded inline so it can never be corrupted by asset pipelines --> 23 <link rel="icon" type="image/png" sizes="32x32" 24 href="data:image/png;base64,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"> 25 <link rel="icon" type="image/png" sizes="16x16" 26 href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADFklEQVR4nF3Sy2tcdQDF8e9v5r7nzkNNpq01TVKbmLZRMZISUCm4kIKiuKiKSha20lUFN9a6sBSUFhHEF4oipUhcpAuLCmqEQEWjFiOWqrVSGiNxtJlMMs977+93Z+7PxRBoPX
26/AB87hCN0NUirmfjhHu9PBdRws08A001iWhWkaWKaJimOGt/UjhGA9RpIkpFIplkrLXLr8N1+dmSfjOWRcB8+z8TMejmPh2Ba37tzKX0tXuHf3OCkhEEJgrEtKKVZWa8ye+ZGM59BJEpIkIYwUuayHkU7z+vFncWyTue9+5p67xtBak1oHZKSoNwI8zyab9chlPfo2b+Dxvffh2BZ9NxVZWCzxyhtTtMKIs/O/AILUeh+pYqq1BjrRJJ2EK8urSCkZu22IeiOgVmuSy3rcPXE7ruuyVq2zXK5gCCFIkoRIKhrNEBXHJFrz5KN72LjhBkr/VNgxMsDvFxd56dWTZH2X3y7+yf7JB7Atq7uBUjFKKcorVXp7CkyfeJmB/k204w6GmebFQ0/x0akZDh99h9K/Kwzd3IdppPE8pwu02x0qaw0G+zdx9PA+PpuZ49z5P7h0eQnDMNg+3M+R5/cx+dge3j1xmo3F67EtE9M0uyNKFdNsBvi+i59xmZr+kofu300UKXp7CoyPbefhJw5x8MBeJnaNYpomrucgBF0gCEIMw2D641k+/eIbTk8d5/OZOXzfo9kMOPvTBQ4eeIRfLyxQLq9R7C1gW2b3SABSKhrNgHQ6xfsnP2Fgy4188NYL1BsBUipc1+bb78+z/5ljCCEo9hSwrgYiqag3WgjA8xyeO/I2QRBy5x0jxHGHN987xezX8zi2RSHv4/serutcC1RrTVbX6iRaA5pjr33ILUNbaAURC4sl8rkMSrXJ53xc28J17GsrbBvczNOTD9IKI2r1FmvVOuWVKlGkyPoekVRUKjVGd2zFdW0c+ypgfGwnhXyWiV2jKBUTRQopFZFUhJGk1YpoBRGV1SrF3uvQWmM7FlprhNZa878kSYKK20jZhcJQEkmJUjFBGDEyPEg+56O15j/VqWvo8lbqoAAAAABJRU5ErkJggg=="> 27 <!-- Real-URL fallback (Google Search reads this); apple-touch for iOS home screen --> 28 <link rel="icon" href="https://auth.digitalmaker.ai/storage/v1/object/public/digital-maker-outputs/favicon-256.png"> 29 <link rel="apple-touch-icon" 30 href="https://auth.digitalmaker.ai/storage/v1/object/public/digital-maker-outputs/favicon-256.png"> 31 <!-- Tags marked data-react-helmet are the home page defaults. Each page's 32 <Helmet> replaces them in the browser, so a page never carries the home 33 page's description alongside its own. Crawlers that don't run JS (social 34 previews) still read these. --> 35 <meta name="generator" content="Digital Maker AI" /> 36 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 37 <title>Digital Maker AI - Create Faceless AI Videos & Digital Products</title> 38 <!-- Organization structured data: tells Google which social profiles are ours (sameAs) --> 39
39<script type="application/ld+json"> 40 { 41 "@context": "https://schema.org", 42 "@graph": [ 43 { 44 "@type": "Organization", 45 "@id": "https://digitalmaker.ai/#organization", 46 "name": "Digital Maker AI", 47 "url": "https://digitalmaker.ai", 48 "logo": "https://auth.digitalmaker.ai/storage/v1/object/public/digital-maker-outputs/favicon-256.png", 49 "description": "Create faceless AI videos and digital products, all in one platform.", 50 "sameAs": [ 51 "https://www.instagram.com/digitalmakerai/", 52 "https://www.youtube.com/@DigitalMaker-AI", 53 "https://www.facebook.com/p/Digital-Maker-AI-61589220783723/" 54 ] 55 }, 56 { 57 "@type": "SoftwareApplication", 58 "@id": "https://digitalmaker.ai/#app", 59 "name": "Digital Maker AI", 60 "url": "https://digitalmaker.ai", 61 "applicationCategory": "MultimediaApplication", 62 "operatingSystem": "Web", 63 "description": "AI faceless video maker: turns a topic into a finished faceless YouTube video or Short with AI script, scene images, voiceover, captions and motion. Also makes eBooks, spreadsheets, planners, images and websites.", 64 "publisher": { "@id": "https://digitalmaker.ai/#organization" }, 65 "offers": [ 66 { "@type": "Offer", "name": "Free", "price": "0", "priceCurrency": "USD", "url": "https://digitalmaker.ai/pricing" }, 67 { "@type": "Offer", "name": "Pro", "price": "29", "priceCurrency": "USD", "url": "https://digitalmaker.ai/pricing" }, 68 { "@type": "Offer", "name": "Premium", "price": "59", "priceCurrency": "USD", "url": "https://digitalmaker.ai/pricing" }, 69 { "@type": "Offer", "name": "Ultimate", "price": "99", "priceCurrency": "USD", "url": "https://digitalmaker.ai/pricing" } 70 ] 71 }, 72 { 73 "@type": "WebSite", 74 "@id": "https://digitalmaker.ai/#website", 75 "name": "Digital Maker AI", 76 "url": "https://digitalmaker.ai", 77 "publisher": { "@id": "https://digitalmaker.ai/#organization" } 78 } 79 ] 80 } 81 </script>
81 82 <!-- Tolt affiliate tracking --> 83
83<script async src="https://files.tlt-cdn.com/tlt.js" data-tolt="pk_rs75ASjsYcrRHbSFjYjsJk5S"></script>
83 84 <!-- Google Ads tag -->
vendor: 66 bytes, lines 84-85
84 85 <script async src="https://www.googletagmanager.com/gtag/js?id=
85AW-18347101597
vendor: 14 bytes, lines 85-86
85"></script> 86
86<script> 87
vendor: 139 bytes, lines 87-90
87window.dataLayer = window.dataLayer || []; 88 function gtag(){dataLayer.push(arguments);} 89 gtag('js', new Date()); 90 gtag('config', '
90AW-18347101597
vendor: 6 bytes, lines 90-91
90'); 91
91</script>
91 92 <!-- First-touch attribution capture. Runs synchronously before React mounts 93 and before any OAuth redirect, so ad clicks are never lost. Only writes 94 on a first touch that actually carries tracking params; never overwrites. --> 95
95<script> 96 (function () { 97 try { 98 var p = new URLSearchParams(window.location.search); 99 var keys = ['utm_source','utm_medium','utm_campaign','utm_content','utm_term','utm_placement','gclid','gbraid','wbraid','gad_source']; 100 var data = {}; 101 var hasAny = false; 102 keys.forEach(function (k) { 103 var v = p.get(k); 104 if (v) { data[k] = v; hasAny = true; } 105 }); 106 if (hasAny && !localStorage.getItem('dm_attribution')) { 107 data.landing_url = window.location.href; 108 data.captured_at = new Date().toISOString(); 109 localStorage.setItem('dm_attribution', JSON.stringify(data)); 110 } 111 } catch (e) { /* no-op */ } 112 })(); 113 </script>
113 114
114<script type="module" crossorigin src="/assets/index-OlFrYGZF.js"></script>
114 115 <link rel="stylesheet" crossorigin href="/assets/index-DE6EvvX5.css"> 116
116<script type="module"> 117window.onerror = (message, source, lineno, colno, errorObj) => { 118 const errorDetails = errorObj ? JSON.stringify({ 119 name: errorObj.name, 120 message: errorObj.message, 121 stack: errorObj.stack, 122 source, 123 lineno, 124 colno, 125 }) : null; 126 127 window.parent.postMessage({ 128 type: 'horizons-runtime-error', 129 message, 130 error: errorDetails 131 }, '*'); 132}; 133</script>
133 134
134<script type="module"> 135const observer = new MutationObserver((mutations) => { 136 for (const mutation of mutations) { 137 for (const addedNode of mutation.addedNodes) { 138 if ( 139 addedNode.nodeType === Node.ELEMENT_NODE && 140 ( 141 addedNode.tagName?.toLowerCase() === 'vite-error-overlay' || 142 addedNode.classList?.contains('backdrop') 143 ) 144 ) { 145 handleViteOverlay(addedNode); 146 } 147 } 148 } 149}); 150 151observer.observe(document.documentElement, { 152 childList: true, 153 subtree: true 154}); 155 156function handleViteOverlay(node) { 157 if (!node.shadowRoot) { 158 return; 159 } 160 161 const backdrop = node.shadowRoot.querySelector('.backdrop'); 162 163 if (backdrop) { 164 const overlayHtml = backdrop.outerHTML; 165 const parser = new DOMParser(); 166 const doc = parser.parseFromString(overlayHtml, 'text/html'); 167 const messageBodyElement = doc.querySelector('.message-body'); 168 const fileElement = doc.querySelector('.file'); 169 const messageText = messageBodyElement ? messageBodyElement.textContent.trim() : ''; 170 const fileText = fileElement ? fileElement.textContent.trim() : ''; 171 const error = messageText + (fileText ? ' File:' + fileText : ''); 172 173 window.parent.postMessage({ 174 type: 'horizons-vite-error', 175 error, 176 }, '*'); 177 } 178} 179</script>
179 180
180<script type="module"> 181const originalConsoleError = console.error; 182const MATCH_LINE_COL_REGEX = /:(\d+):(\d+)\)?\s*$/; // regex to match the :lineNum:colNum 183const MATCH_AT_REGEX = /^\s*at\s+(?:async\s+)?(?:.*?\s+)?\(?/; // regex to remove the 'at' keyword and any 'async' or function name 184const MATCH_PATH_REGEX = /^\//; // regex to remove the leading slash 185 186function parseStackFrameLine(line) { 187 const lineColMatch = line.match(MATCH_LINE_COL_REGEX); 188 if (!lineColMatch) return null; 189 const [, lineNum, colNum] = lineColMatch; 190 const suffix = `:${lineNum}:${colNum}`; 191 const idx = line.lastIndexOf(suffix); 192 if (idx === -1) return null; 193 const before = line.substring(0, idx); 194 const path = before.replace(MATCH_AT_REGEX, '').trim(); 195 196 if (!path) return null; 197 198 try { 199 const pathname = new URL(path).pathname; 200 const filePath = pathname.replace(MATCH_PATH_REGEX, '') || pathname; 201 return `${filePath}:${lineNum}:${colNum}`; 202 } catch (e) { 203 const filePath = path.replace(MATCH_PATH_REGEX, '') || path; 204 return `${filePath}:${lineNum}:${colNum}`; 205 } 206} 207 208function getFilePathFromStack(stack, skipFrames = 0) { 209 if (!stack || typeof stack !== 'string') return null; 210 const lines = stack.split('\n').slice(1); 211 212 const frames = lines.map(line => parseStackFrameLine(line.replace(/\r$/, ''))).filter(Boolean); 213 214 return frames[skipFrames] ?? null; 215} 216 217function formatConsoleMessage(args, skipStackFrames = 1) { 218 let messageString = ''; 219 let filePath = null; 220 221 for (let i = 0; i < args.length; i++) { 222 const arg = args[i]; 223 if (arg instanceof Error) { 224 filePath = getFilePathFromStack(arg.stack, 0); 225 messageString = `${arg.name}: ${arg.message}`; 226 if (filePath) { 227 messageString = `${messageString} at ${filePath}`; 228 } 229 break; 230 } 231 } 232 233 if (!messageString) { 234 // Never let logging throw: React elements, DOM nodes and many error objects 235 // are circular, and JSON.stringify on them threw inside console.error, 236 // crashing the page to a black screen for customers (2026-10-05). 237 messageString = args.map(arg => { 238 if (arg === null || typeof arg !== 'object') return String(arg); 239 try { return JSON.stringify(arg); } catch (e) { return Object.prototype.toString.call(arg); } 240 }).join(' '); 241 const stack = new Error().stack; 242 filePath = getFilePathFromStack(stack, skipStackFrames); 243 if (filePath) { 244 messageString = `${messageString} at ${filePath}`; 245 } 246 } 247 248 return messageString; 249} 250 251// Only report to an embedding editor (the old Horizons builder iframe). On the 252// live site there is no parent window, and these wrappers must never throw. 253const IS_EMBEDDED = (() => { try { return window.self !== window.top; } catch (e) { return true; } })(); 254 255console.error = function(...args) { 256 originalConsoleError.apply(console, args); 257 if (!IS_EMBEDDED) return; 258 try { 259 window.parent.postMessage({ 260 type: 'horizons-console-error', 261 error: formatConsoleMessage(args, 1) 262 }, '*'); 263 } catch (e) { /* logging must never break the page */ } 264}; 265 266const originalConsoleWarn = console.warn; 267 268console.warn = function(...args) { 269 originalConsoleWarn.apply(console, args); 270 if (!IS_EMBEDDED) return; 271 try { 272 window.parent.postMessage({ 273 type: 'horizons-console-warn', 274 warning: formatConsoleMessage(args, 1) 275 }, '*'); 276 } catch (e) { /* logging must never break the page */ } 277}; 278</script>
278 279
279<script type="module"> 280// Prevents authentication failures (PB returns 400) from crashing the app â use console.info, not console.error/PM2. 281// [urlPattern, bodyPattern] tuples; align with sandboxErrorUtils ERROR_BLACKLIST. 282const BENIGN_FETCH_ERRORS = [ 283 [/hcgi\/platform\/api\/collections\/.*auth-with-password.*/i, /Failed to authenticate/i], 284 [/hcgi\/api\//i, /Insufficient credits/i], 285 // A user-supplied integration secret (Stripe/PayPal/Twilio/...) is not set yet â an expected setup state, not a bug to fix. 286 [/hcgi\/api\//i, /INTEGRATION_NOT_CONFIGURED/i], 287]; 288 289function isBenignFetchError(url, body) { 290 return BENIGN_FETCH_ERRORS.some(([urlPattern, bodyPattern]) => 291 urlPattern.test(url) && (!bodyPattern || bodyPattern.test(body))); 292} 293 294const PLATFORM_URL_PATTERN = /hcgi\/platform\//i; 295const VALIDATION_CODE_TEXT_PATTERN = /validation_/; 296 297function hasValidationCode(value) { 298 if (value == null) { 299 return false; 300 } 301 if (typeof value === 'string') { 302 return value.startsWith('validation_'); 303 } 304 if (Array.isArray(value)) { 305 return value.some(hasValidationCode); 306 } 307 if (typeof value === 'object') { 308 return Object.values(value).some(hasValidationCode); 309 } 310 return false; 311} 312 313function isValidationFetchWarning(url, body) { 314 if (!PLATFORM_URL_PATTERN.test(url)) { 315 return false; 316 } 317 try { 318 return hasValidationCode(JSON.parse(body)); 319 } catch { 320 return VALIDATION_CODE_TEXT_PATTERN.test(body); 321 } 322} 323 324const originalFetch = window.fetch; 325 326window.fetch = function(...args) { 327 const url = args[0] instanceof Request ? args[0].url : args[0]; 328 329 // Skip WebSocket URLs 330 if (url.startsWith('ws:') || url.startsWith('wss:')) { 331 return originalFetch.apply(this, args); 332 } 333 334 return originalFetch.apply(this, args) 335 .then(async response => { 336 const contentType = response.headers.get('Content-Type') || ''; 337 338 // Exclude HTML document responses 339 const isDocumentResponse = 340 contentType.includes('text/html') || 341 contentType.includes('application/xhtml+xml'); 342 343 if (!response.ok && !isDocumentResponse) { 344 const responseClone = response.clone(); 345 const errorFromRes = await responseClone.text(); 346 const requestUrl = response.url; 347 const errorMessage = `Fetch error from ${requestUrl}: ${errorFromRes}`; 348 349 if (isBenignFetchError(requestUrl, errorFromRes)) { 350 console.info(errorMessage); 351 } else if (isValidationFetchWarning(requestUrl, errorFromRes)) { 352 console.warn(errorMessage); 353 } else { 354 console.error(errorMessage); 355 } 356 } 357 358 return response; 359 }) 360 .catch(error => { 361 if (!url.match(/.html?$/i)) { 362 // Cancelled in-flight fetch (e.g. preview reload) â not a server failure. 363 if (error?.name === 'AbortError') { 364 console.info(error); 365 } else { 366 console.error(error); 367 } 368 } 369 370 throw error; 371 }); 372}; 373</script>
373 374
374<script type="module"> 375if (window.navigation && window.self !== window.top) { 376 window.navigation.addEventListener('navigate', (event) => { 377 const url = event.destination.url; 378 379 try { 380 const destinationUrl = new URL(url); 381 const destinationOrigin = destinationUrl.origin; 382 const currentOrigin = window.location.origin; 383 384 if (destinationOrigin === currentOrigin) { 385 return; 386 } 387 } catch (error) { 388 return; 389 } 390 391 window.parent.postMessage({ 392 type: 'horizons-navigation-error', 393 url, 394 }, '*'); 395 }); 396} 397</script>
397 398</head> 399 400<body> 401 <div id="root"></div> 402</body> 403 404</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.