File size: 126,610 Bytes
f201243 d4a4da7 f201243 d4a4da7 254d33c d4a4da7 f201243 d4a4da7 f201243 d4a4da7 026f283 d4a4da7 f201243 d4a4da7 f201243 c5771b6 f201243 d4a4da7 f201243 b3adf58 f201243 254d33c f201243 d4a4da7 f201243 d4a4da7 254d33c f201243 254d33c f201243 254d33c d4a4da7 026f283 f201243 026f283 254d33c 026f283 254d33c f201243 254d33c f201243 505ff55 f201243 2c9d10c a1baa8d 2c9d10c a1baa8d f201243 a1baa8d f201243 a1baa8d f201243 254d33c f201243 254d33c f201243 254d33c f201243 254d33c f201243 254d33c f201243 254d33c f201243 254d33c d4a4da7 f201243 505ff55 026f283 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 bf50e29 f201243 d4a4da7 f201243 505ff55 f201243 addcf34 c5771b6 f201243 addcf34 c5771b6 f201243 505ff55 f201243 d4a4da7 505ff55 d4a4da7 c5771b6 f201243 d4a4da7 f201243 c5771b6 f201243 026f283 addcf34 026f283 f201243 c5771b6 026f283 505ff55 f201243 026f283 f201243 026f283 f201243 026f283 f201243 026f283 f201243 026f283 f201243 505ff55 026f283 505ff55 f201243 026f283 505ff55 2c9d10c 026f283 f201243 026f283 f201243 505ff55 f201243 026f283 f201243 026f283 f201243 026f283 f201243 d4a4da7 f201243 d4a4da7 f201243 d4a4da7 f201243 2c9d10c 505ff55 f201243 505ff55 f201243 505ff55 f201243 a1baa8d f201243 d4a4da7 a1baa8d 2c9d10c a1baa8d d4a4da7 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 505ff55 f201243 1afa519 f201243 1afa519 f201243 1afa519 f201243 1afa519 f201243 1afa519 f201243 5106651 026f283 5106651 026f283 4a56a0b 026f283 4a56a0b 026f283 4a56a0b 533051b f201243 4a56a0b f201243 4a56a0b 5106651 4a56a0b 5106651 2c9d10c 5106651 2c9d10c 5106651 026f283 5106651 d4a4da7 5106651 f201243 4a56a0b f201243 2c9d10c f201243 4a56a0b f201243 26e8ad9 f201243 d4a4da7 26e8ad9 f201243 26e8ad9 f201243 26e8ad9 505ff55 026f283 26e8ad9 026f283 26e8ad9 026f283 26e8ad9 026f283 26e8ad9 f201243 26e8ad9 f201243 26e8ad9 f201243 26e8ad9 f201243 26e8ad9 f201243 26e8ad9 f201243 26e8ad9 f201243 26e8ad9 026f283 26e8ad9 f201243 26e8ad9 d4a4da7 f201243 c5771b6 f201243 c5771b6 f201243 c5771b6 f201243 505ff55 f201243 2c9d10c addcf34 c5771b6 d4a4da7 c5771b6 f201243 505ff55 f201243 addcf34 c5771b6 f201243 505ff55 533051b d4a4da7 f201243 d4a4da7 f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 1afa519 f201243 4928a87 f201243 4928a87 505ff55 4928a87 f201243 d4a4da7 f201243 68556d7 f201243 a162882 c5771b6 f201243 68556d7 f201243 68556d7 f201243 68556d7 f201243 68556d7 f201243 68556d7 f201243 68556d7 f201243 68556d7 f201243 505ff55 f201243 505ff55 a162882 c5771b6 f201243 addcf34 f201243 a162882 f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 4928a87 f201243 4928a87 505ff55 4928a87 1afa519 4928a87 f201243 d4a4da7 f201243 addcf34 f201243 026f283 f201243 026f283 f201243 026f283 addcf34 f201243 026f283 f201243 8ffe335 f201243 8ffe335 f201243 8ffe335 f201243 026f283 addcf34 026f283 f201243 026f283 f201243 026f283 f201243 254d33c f201243 026f283 254d33c f201243 026f283 f201243 254d33c f201243 026f283 533051b 026f283 dd2695a f201243 026f283 f201243 d4a4da7 026f283 f201243 2c9d10c f201243 2c9d10c 533051b 2c9d10c f201243 2c9d10c 254d33c d4a4da7 254d33c 2c9d10c 254d33c f201243 533051b f201243 533051b f201243 533051b 026f283 533051b f201243 026f283 e5a550b 26e8ad9 026f283 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 533051b f201243 4928a87 f201243 4928a87 f201243 4928a87 505ff55 4928a87 f201243 533051b f201243 533051b f201243 9de719c f201243 9de719c f201243 68556d7 d4a4da7 68556d7 026f283 68556d7 026f283 68556d7 d4a4da7 f201243 505ff55 a162882 c5771b6 f201243 505ff55 f201243 505ff55 f201243 a162882 f201243 a162882 f201243 026f283 f201243 026f283 f201243 c5771b6 f201243 026f283 f201243 026f283 f201243 026f283 f201243 026f283 f201243 a162882 f201243 a162882 f201243 a162882 f201243 505ff55 f201243 a162882 f201243 a162882 f201243 026f283 533051b a162882 f201243 533051b a162882 f201243 a162882 f201243 26e8ad9 f201243 d4a4da7 f201243 addcf34 c5771b6 f201243 533051b f201243 addcf34 f201243 533051b f201243 533051b addcf34 f201243 533051b c5771b6 533051b c5771b6 533051b f201243 533051b f201243 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 |
"""
Ad Generator Service
Generates high-converting ad creatives using psychological triggers, LLM copy,
and image generation. Uses maximum randomization for variety and saves to the
Neon database with optional R2 image storage.
"""
import asyncio
import json
import os
import random
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional
from config import settings
from data import auto_insurance, glp1, home_insurance
from data.frameworks import (
get_all_frameworks,
get_framework,
get_framework_visual_guidance,
get_frameworks_for_niche,
)
from data.ecom_verticals import get_random_vertical, get_verticals_for_prompt
from data.hooks import get_power_words, get_random_cta as get_hook_cta, get_random_hook_style
from data.triggers import get_random_trigger, get_trigger_combination, get_triggers_for_niche
from data.visuals import (
get_color_palette,
get_random_camera_angle,
get_random_composition,
get_random_lighting,
get_random_mood,
get_random_visual_style,
)
from services.generator_prompts import (
get_headline_formulas,
get_numbers_section,
get_trending_section,
get_trending_image_guidance,
)
from services.image import image_service
from services.llm import llm_service
from services.matrix import matrix_service
try:
from services.database import db_service
except ImportError:
db_service = None
try:
from services.r2_storage import get_r2_storage
r2_storage_available = True
except ImportError:
r2_storage_available = False
try:
from services.third_flow import third_flow_service
third_flow_available = True
except ImportError:
third_flow_available = False
try:
from services.trend_monitor import trend_monitor
trend_monitor_available = True
except ImportError:
trend_monitor_available = False
# -----------------------------------------------------------------------------
# Constants
# -----------------------------------------------------------------------------
NICHE_DATA = {
"home_insurance": home_insurance.get_niche_data,
"glp1": glp1.get_niche_data,
"auto_insurance": auto_insurance.get_niche_data,
}
# Age brackets for identity targeting (proven high-CTR pattern)
AGE_BRACKETS = [
{"label": "21-40", "color": "yellow/gold button"},
{"label": "41-64", "color": "blue button"},
{"label": "65+", "color": "red button"},
{"label": "50-60 years", "color": "gray box"},
{"label": "60-70 years", "color": "gray box"},
{"label": "70+ years old", "color": "gray box"},
]
# Vintage film visual styles
VINTAGE_FILM_STYLES = [
"grainy 8mm home movie footage from the 1970s, warm faded colors, light leaks",
"old VHS tape recording with tracking lines, fuzzy edges, dated look",
"vintage 35mm film photograph, visible grain, slightly overexposed highlights",
"retro Super 8 film aesthetic, soft focus, amber tint, nostalgic",
"aged polaroid photograph style, faded colors, white border, worn edges",
"1960s Kodachrome film look, saturated but faded reds and yellows",
"old TV broadcast footage, scan lines, slight color bleeding",
"vintage sepia-toned photograph, crackled texture, antique feel",
"worn 16mm documentary footage, high grain, muted earth tones",
"degraded archival footage look, dust particles, scratches, light decay",
]
# Film damage effects for authenticity
FILM_DAMAGE_EFFECTS = [
"film scratches, dust specks, light leaks in corners",
"vignette darkening at edges, slight color shift",
"horizontal scan lines, minor static noise",
"subtle frame jitter effect, worn sprocket marks",
"chemical staining, uneven development marks",
"faded edges with light burn, aged patina",
"dust particles floating, hair gate scratches",
"color bleeding at high contrast edges, emulsion damage",
]
class AdGenerator:
"""
Generates ad creatives: copy (LLM) + images, with randomization and DB/R2 save.
Sections:
- Init & config: output dir, local save, niche cache
- Niche & strategy: get niche data, compatible strategies, hooks, visuals
- CTAs & numbers: generate CTAs, prices, niche numbers
- Copy prompt: _build_copy_prompt (angle × concept, frameworks)
- Image prompt: _build_image_prompt, _refine_image_prompt
- Public: generate_ad, generate_ad_with_matrix, generate_ad_extensive, generate_batch
- Matrix: _build_matrix_ad_prompt, _build_matrix_image_prompt
- Refine: refine_custom_angle_or_concept
"""
# --- Init & config ---
def __init__(self):
"""Initialize the generator."""
self.output_dir = settings.output_dir
os.makedirs(self.output_dir, exist_ok=True)
# Cache niche data to avoid repeated function calls
self._niche_data_cache: Dict[str, Dict[str, Any]] = {}
def _should_save_locally(self) -> bool:
"""
Determine if images should be saved locally based on environment settings.
Returns:
True if images should be saved locally, False otherwise
"""
# In production, only save locally if explicitly enabled
if settings.environment.lower() == "production":
return settings.save_images_locally
# In development, always save locally
return True
def _save_image_locally(self, image_bytes: bytes, filename: str) -> Optional[str]:
"""
Conditionally save image locally based on environment settings.
Args:
image_bytes: Image data to save
filename: Filename for the image
Returns:
Filepath if saved, None otherwise
"""
if not self._should_save_locally():
return None
try:
filepath = os.path.join(self.output_dir, filename)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "wb") as f:
f.write(image_bytes)
return filepath
except Exception as e:
print(f"Warning: Failed to save image locally: {e}")
return None
# ========================================================================
# DATA RETRIEVAL & CACHING METHODS
# ========================================================================
# --- Niche & strategy ---
def _get_minimal_niche_data_for_custom(self, niche: str) -> Dict[str, Any]:
"""Return minimal niche data for custom/others niche (no raise). Used when niche is not in NICHE_DATA."""
label = (niche or "").replace("_", " ").title() or "Custom"
return {
"niche": niche,
"strategies": {},
"strategy_names": [],
"creative_directions": ["professional", "clean", "trustworthy"],
"visual_moods": ["neutral", "approachable"],
"niche_guidance": f"Focus on the {label} niche. Use authentic, low-production visuals.",
"image_guidance": f"Images should be appropriate for {label}. Authentic, relatable visuals.",
"image_niche_guidance_short": f"NICHE: {label}",
"number_config": {},
"price_config": {},
"prompt_sanitization_replacements": [],
"visual_library": {},
"image_creative_prompts": [],
"all_hooks": [],
"all_visual_styles": [],
"copy_templates": [],
}
def _get_niche_data(self, niche: str) -> Dict[str, Any]:
"""Load data for a specific niche (cached). For custom/others niche, returns minimal data instead of raising."""
if niche not in self._niche_data_cache:
if niche in NICHE_DATA:
self._niche_data_cache[niche] = NICHE_DATA[niche]()
else:
# Custom/others niche (e.g. "Roofing Programme") — use minimal data so image refinement/save don't crash
self._niche_data_cache[niche] = self._get_minimal_niche_data_for_custom(niche)
return self._niche_data_cache[niche]
# ========================================================================
# STRATEGY & COMPATIBILITY METHODS
# ========================================================================
def _get_framework_strategy_compatibility(self, framework_key: str, strategy_name: str) -> float:
"""
Calculate compatibility score between framework and strategy.
Returns score 0.0-1.0, higher = better match.
"""
# Framework-Strategy compatibility matrix
compatibility_map = {
"breaking_news": {
"accusation_opener": 0.9,
"curiosity_gap": 0.95,
"price_focused": 0.8,
"proof_based": 0.7,
"authority_backed": 0.85,
"urgent": 0.95,
},
"testimonial": {
"proof_based": 0.95,
"authority_backed": 0.8,
"social_proof": 0.9,
},
"before_after": {
"proof_based": 0.95,
"transformation": 0.9,
"price_focused": 0.8,
},
"problem_solution": {
"accusation_opener": 0.85,
"problem_awareness": 0.95,
"solution_focused": 0.9,
},
"authority": {
"authority_backed": 0.95,
"expert_recommended": 0.9,
"proof_based": 0.8,
},
"lifestyle": {
"aspirational": 0.9,
"identity_targeted": 0.85,
"emotional": 0.8,
},
"comparison": {
"price_focused": 0.9,
"proof_based": 0.8,
"comparison_logic": 0.95,
},
"storytelling": {
"emotional": 0.9,
"relatable": 0.85,
"transformation": 0.8,
},
"mobile_post": {
"convenience": 0.9,
"quick_action": 0.85,
"simple": 0.9,
},
"educational": {
"authority_backed": 0.8,
"curiosity_gap": 0.85,
"informative": 0.9,
},
}
# Normalize strategy name (remove spaces, lowercase)
strategy_key = strategy_name.lower().replace(" ", "_").replace("-", "_")
# Check direct match
if framework_key in compatibility_map:
if strategy_key in compatibility_map[framework_key]:
return compatibility_map[framework_key][strategy_key]
# Default compatibility (still usable, just not optimal)
return 0.6
def _select_compatible_strategies(self, niche_data: Dict, framework_key: str, count: int = 2) -> List[Dict]:
"""
Select strategies that are compatible with the chosen framework.
Prioritizes high-compatibility matches but ensures variety.
"""
all_strategies = list(niche_data.get("strategies", {}).values())
if not all_strategies:
return []
# Score all strategies
scored_strategies = [
(strategy, self._get_framework_strategy_compatibility(framework_key, strategy["name"]))
for strategy in all_strategies
]
# Sort by compatibility (highest first)
scored_strategies.sort(key=lambda x: x[1], reverse=True)
# Select mix: 70% top compatible, 30% random for variety
top_count = max(1, int(count * 0.7))
selected = []
# Add top compatible
for strategy, score in scored_strategies[:top_count]:
selected.append(strategy)
# Add random for variety (if we need more)
remaining = count - len(selected)
if remaining > 0:
remaining_strategies = [s for s, _ in scored_strategies[top_count:]]
if remaining_strategies:
selected.extend(random.sample(remaining_strategies, min(remaining, len(remaining_strategies))))
return selected[:count]
def _random_strategies(self, niche_data: Dict, count: int = 2) -> List[Dict]:
"""Randomly select strategies for maximum variety."""
strategy_names = random.sample(niche_data["strategy_names"], min(count, len(niche_data["strategy_names"])))
return [niche_data["strategies"][name] for name in strategy_names]
def _random_hooks(self, strategies: List[Dict], count: int = 3) -> List[str]:
"""Randomly select hooks from the chosen strategies (optimized list building)."""
# Optimized: use list comprehension instead of extend in loop
all_hooks = [hook for strategy in strategies for hook in strategy["hooks"]]
return random.sample(all_hooks, min(count, len(all_hooks))) if all_hooks else []
# ========================================================================
# VISUAL SELECTION METHODS
# ========================================================================
def _get_visual_library_for_niche(self, niche: str) -> Dict[str, List[str]]:
"""
Get visual library categories for a niche.
Returns dict mapping category names to visual descriptions.
"""
niche_data = self._get_niche_data(niche)
return niche_data.get("visual_library", {})
def _select_visuals_from_library(self, niche: str, strategy_name: str, count: int = 2) -> List[str]:
"""
Select visuals from the comprehensive visual library based on strategy.
This ensures we use the full visual library, not just strategy-specific visuals.
"""
visual_library = self._get_visual_library_for_niche(niche)
if not visual_library:
# Fallback to strategy visuals
return []
# Map strategies to visual categories (auto_insurance: 1:1 with the 6 ad formats)
auto_insurance_strategy_categories = {
"official_notification": ["official_notification_style"],
"social_post": ["social_post_style"],
"coverage_tiers": ["coverage_tiers_style"],
"car_brand_grid": ["car_brand_grid_style"],
"gift_card_cta": ["gift_card_cta_style"],
"savings_urgency": ["savings_urgency_style"],
}
default_strategy_to_category = {
"accusation_opener": ["problem_risk", "disaster_fear"],
"curiosity_gap": ["text_first", "minimal_symbolic"],
"price_focused": ["comparison_choice", "mortgage_bank"],
"proof_based": ["relief", "protection_safety"],
"authority_backed": ["mortgage_bank", "protection_safety"],
"identity_targeted": ["first_time_homebuyer", "lifestyle"],
"family_emotional": ["family_emotional", "protection_safety"],
"fear_based": ["disaster_fear", "problem_risk"],
"relief": ["relief", "protection_safety"],
}
strategy_to_category = (
auto_insurance_strategy_categories
if niche == "auto_insurance"
else default_strategy_to_category
)
# Normalize strategy name
strategy_key = strategy_name.lower().replace(" ", "_").replace("-", "_")
# Get relevant categories (only use categories that exist in this niche's library)
requested = strategy_to_category.get(strategy_key, list(visual_library.keys()))
categories = [c for c in requested if c in visual_library] or list(visual_library.keys())
# Select visuals from relevant categories
selected_visuals = []
for category in categories[:2]: # Use top 2 categories
if category in visual_library:
visuals = visual_library[category]
if visuals:
selected_visuals.extend(random.sample(visuals, min(1, len(visuals))))
# If we need more, add random from any category (optimized: use list comprehension)
if len(selected_visuals) < count:
all_visuals = [v for visuals in visual_library.values() for v in visuals]
remaining = count - len(selected_visuals)
if all_visuals:
selected_visuals.extend(random.sample(all_visuals, min(remaining, len(all_visuals))))
return selected_visuals[:count]
def _random_visual_styles(self, strategies: List[Dict], count: int = 2, niche: str = "", use_library: bool = True) -> List[str]:
"""
Select visual scene descriptions from strategies and/or visual library.
Note: These are SCENE DESCRIPTIONS (what to show), not aesthetic styles.
Aesthetic styles come from data/visuals.py VISUAL_STYLES.
Args:
strategies: List of strategy dicts
count: Number of visuals to select
niche: Niche name for visual library access
use_library: Whether to use comprehensive visual library (improvement)
"""
# Optimized: use list comprehension instead of extend in loops
# Get strategy-specific visuals
all_styles = [style for strategy in strategies for style in strategy.get("visual_styles", [])]
# Add visual library visuals (improvement)
if use_library and niche:
library_visuals = [
v for strategy in strategies
for v in self._select_visuals_from_library(niche, strategy.get("name", ""), count=1)
]
all_styles.extend(library_visuals)
# Remove duplicates while preserving order (optimized using dict.fromkeys)
unique_styles = list(dict.fromkeys(all_styles))
return random.sample(unique_styles, min(count, len(unique_styles))) if unique_styles else []
# ========================================================================
# NICHE & CONTENT CONFIGURATION METHODS
# ========================================================================
# --- CTAs & numbers ---
def _get_niche_specific_guidance(self, niche: str) -> str:
"""Get niche-specific guidance for the prompt."""
niche_data = self._get_niche_data(niche)
return niche_data.get("niche_guidance", "")
async def _generate_ctas_async(
self, niche: str, framework_name: Optional[str] = None
) -> List[str]:
"""
Generate 5–8 CTAs for the niche (and optional framework) via LLM.
Returns empty list on failure; caller uses single default when empty.
"""
niche_label = (niche or "").replace("_", " ").title() or "general advertising"
context = f"Niche: {niche_label}"
if framework_name:
context += f". Ad format/framework: {framework_name}"
prompt = f"""Generate 5 to 8 short call-to-action (CTA) button/link phrases for a paid ad.
{context}
Rules:
- Generate diverse, scroll-stopping CTAs; niche is optional context; any tone or style allowed.
- Return ONLY a JSON object with one key "ctas" containing an array of strings. No other text."""
try:
result = await llm_service.generate_json(prompt=prompt, temperature=0.7)
ctas = result.get("ctas") if isinstance(result, dict) else None
if isinstance(ctas, list) and len(ctas) > 0:
return [str(c).strip() for c in ctas if c]
except Exception:
pass
return []
def _generate_specific_price(self, niche: str) -> str:
"""
Generate price guidance for the AI.
The AI will decide whether to include prices and what amounts to use.
"""
niche_data = self._get_niche_data(niche)
default = "Use contextually appropriate prices if the ad format requires them. Make them oddly specific (not rounded) for believability."
return niche_data.get("price_config", {}).get("guidance", default)
def _generate_niche_numbers(self, niche: str) -> Dict[str, str]:
"""Generate niche-specific numbers for authenticity from niche number_config."""
niche_data = self._get_niche_data(niche)
config = niche_data.get("number_config", {})
if not config:
return {}
num_type = config.get("type", "savings")
labels = config.get("labels", {})
if num_type == "savings":
before_range = config.get("before_range", [1200, 2400])
savings_range = config.get("savings_pct_range", [0.50, 0.75])
before = random.randint(before_range[0], before_range[1])
savings_pct = random.uniform(savings_range[0], savings_range[1])
after = int(before * (1 - savings_pct))
return {
"type": "savings",
"before": f"${before:,}/year",
"after": f"${after}/year",
"difference": f"${before - after:,}",
"metric": labels.get("metric", "savings per year"),
}
if num_type == "weight_loss":
before_range = config.get("before_range", [180, 280])
loss_range = config.get("loss_range", [25, 65])
days_options = config.get("days_options", [60, 90, 120])
sizes_range = config.get("sizes_range", [2, 5])
before_weight = random.randint(before_range[0], before_range[1])
lbs_lost = random.randint(loss_range[0], loss_range[1])
after_weight = before_weight - lbs_lost
days = random.choice(days_options)
sizes_dropped = random.randint(sizes_range[0], sizes_range[1])
return {
"type": "weight_loss",
"before": f"{before_weight} lbs",
"after": f"{after_weight} lbs",
"difference": f"{lbs_lost} lbs",
"days": f"{days} days",
"sizes": f"{sizes_dropped} dress sizes",
"metric": labels.get("metric", "pounds lost"),
}
return {}
# --- Copy prompt ---
def _build_copy_prompt(
self,
niche: str,
niche_data: Dict,
strategies: List[Dict],
hooks: List[str],
creative_direction: str,
framework: str,
framework_data: Dict[str, Any],
cta: str,
trigger_data: Dict[str, Any] = None,
trigger_combination: Dict[str, Any] = None,
power_words: List[str] = None,
angle: Dict[str, Any] = None,
concept: Dict[str, Any] = None,
target_audience: Optional[str] = None,
offer: Optional[str] = None,
trending_context: Optional[str] = None,
) -> str:
"""
Build professional LLM prompt for ad copy generation.
Uses angle × concept matrix approach for psychological targeting.
Can optionally incorporate trending topics for increased relevance.
"""
strategy_names = [s["name"] for s in strategies]
strategy_descriptions = [f"- {s['name']}: {s['description']}" for s in strategies]
niche_guidance = self._get_niche_specific_guidance(niche)
# Same framework drives ad idea, copy angle, AND visual format (no separate container)
price_guidance = self._generate_specific_price(niche)
niche_numbers = self._generate_niche_numbers(niche)
age_bracket = random.choice(AGE_BRACKETS)
# Numbers and headline formulas from shared prompt content
num_type = niche_data.get("number_config", {}).get("type", "savings")
numbers_section = get_numbers_section(
niche, num_type, niche_numbers, age_bracket, price_guidance
)
headline_formulas = get_headline_formulas(niche, num_type)
trending_section = get_trending_section(trending_context)
prompt = f"""You are an elite direct-response copywriter who has reverse-engineered hundreds of 7-8 figure Facebook ad campaigns. You understand the psychology of scroll-stopping creatives that bypass ad-blindness and trigger immediate emotional response.
=== CONTEXT ===
NICHE: {niche.replace("_", " ").title()}
ADVERTISING FRAMEWORK: {framework}
FRAMEWORK DESCRIPTION: {framework_data.get('description', '')}
FRAMEWORK TONE: {framework_data.get('tone', '')}
FRAMEWORK VISUAL STYLE: {framework_data.get('visual_style', '')}
FRAMEWORK HEADLINE STYLE: {framework_data.get('headline_style', '') or 'N/A'}
CREATIVE DIRECTION: {creative_direction}
CALL-TO-ACTION: {cta}
{trending_section}
=== ANGLE × CONCEPT (inspiration—invent or extend) ===
ANGLE: {angle.get('name') if angle else 'N/A'} | Trigger: {angle.get('trigger') if angle else 'N/A'} | Example: "{angle.get('example') if angle else 'N/A'}"
CONCEPT: {concept.get('name') if concept else 'N/A'} | Structure: {concept.get('structure') if concept else 'N/A'} | Visual: {concept.get('visual') if concept else 'N/A'}
Use the above or invent your own triggers/angles/concepts.
{f'=== USER INPUTS ===' if target_audience or offer else ''}
{f'TARGET AUDIENCE: {target_audience}' if target_audience else ''}
{f'OFFER: {offer}' if offer else ''}
Target audience can be the given one or any hyperrealistic segment you invent (including outside the niche). You may invent or mix demographics, psychographics, and life stages for maximum diversity and scroll-stopping relevance.
For variety, use angles, concepts, and motivators suited to different ecom verticals: fashion, beauty, supplements, fitness, electronics, home goods, pets, food & beverage, jewelry, subscription. This creative can lean into: {get_random_vertical()['name']}.
=== FRAMEWORK VISUAL FORMAT (same framework for copy + image) ===
FRAMEWORK: {framework_data.get('name', framework)}
DESCRIPTION: {framework_data.get('description', '')}
VISUAL GUIDANCE: {get_framework_visual_guidance(framework_data.get('key', ''))}
FONT STYLE: {framework_data.get('font_style', '')}
COLORS: {', '.join(f'{k}: {v}' for k, v in framework_data.get('colors', {}).items())}
AUTHENTICITY TIPS: {', '.join(framework_data.get('authenticity_tips', [])[:3])}
{numbers_section}
=== PSYCHOLOGICAL STRATEGIES TO DEPLOY ===
{chr(10).join(strategy_descriptions)}
=== PSYCHOLOGICAL TRIGGERS ===
PRIMARY TRIGGER: {trigger_data.get('name', 'N/A') if trigger_data else 'N/A'}
DESCRIPTION: {trigger_data.get('description', '') if trigger_data else ''}
COPY ANGLES: {', '.join(trigger_data.get('copy_angles', [])[:3]) if trigger_data else 'N/A'}
TRIGGER COMBINATION: {trigger_combination.get('name', 'N/A') if trigger_combination else 'N/A'}
COMBINATION DESCRIPTION: {trigger_combination.get('description', '') if trigger_combination else ''}
=== POWER WORDS TO USE ===
Incorporate these power words naturally: {', '.join(power_words) if power_words else 'N/A'}
=== HOOK INSPIRATION (create your own powerful variation) ===
{chr(10).join(f'- "{hook}"' for hook in hooks)}
{niche_guidance}
{headline_formulas}
Use as inspiration only; invent new scroll-stopping or clickbait headlines as needed.
=== YOUR MISSION ===
Create a scroll-stopping Facebook ad that looks like organic content. Use the angle/concept/trigger above (or invent your own). No restrictions—clickbait, bold statements, and any audience are allowed.
=== OUTPUT REQUIREMENTS ===
1. HEADLINE (The "Arrest")
- MAXIMUM 10 words; instant pattern interrupt. Use the angle and trigger above.
- Include numbers/prices only when they strengthen the hook; use oddly specific amounts (e.g. $97.33). Include demographic targeting where appropriate. Be SPECIFIC and EMOTIONAL.
2. PRIMARY TEXT (The "Agitation")
- 2-3 punchy sentences that amplify the emotional hook. Reference the demographic; create urgency; make them FEEL the pain or desire. Oddly specific numbers when they fit.
3. DESCRIPTION (The "Payoff")
- ONE powerful sentence (max 10 words). Create action urgency. Oddly specific metrics when they enhance the message.
4. IMAGE BRIEF (CRITICAL - must match {framework_data.get('name', framework)} framework style)
- Follow the concept above: {concept.get('structure') if concept else 'authentic visual'}
- Describe the scene for the {framework_data.get('name', framework)} framework ONLY
- Visual guidance: {get_framework_visual_guidance(framework_data.get('key', ''))}
- The image should look like ORGANIC CONTENT, not an ad
- Include: setting, props, mood. People are OPTIONAL—creatives can be product-only, layout-only, document-only, or text-only; only add people when the concept clearly calls for them.
- Follow framework authenticity tips: {', '.join(framework_data.get('authenticity_tips', [])[:2])}
- CRITICAL: Use ONLY {framework_data.get('name', framework)} format - DO NOT mix with other formats
- {f"If chat-style framework (e.g. iMessage, WhatsApp): Include 2-4 readable, coherent messages related to {niche.replace('_', ' ').title()}. Use the headline or a variation as one message." if 'chat_style' in framework_data.get('tags', []) else ""}
- {f"If document-style framework (e.g. memo, email): Include readable, properly formatted text related to {niche.replace('_', ' ').title()}." if 'document_style' in framework_data.get('tags', []) else ""}
- FOR AUTO INSURANCE: Describe ONLY one of these 6 ad-format layouts: (1) official notification (seal, rate buttons), (2) social post card, (3) coverage tier panels, (4) car brand grid, (5) gift card CTA, (6) savings/urgency (yellow, CONTACT US). No other creative types. Do NOT describe testimonial portraits, couples, speech bubbles, quote bubbles, or people holding documents. Do NOT describe elderly or senior people. Typography, layout, prices, and buttons only. All text in the image must be readable and correctly spelled; no gibberish.
- FOR HOME INSURANCE: Document, savings proof, home setting. People optional (e.g. document on table only, or person with document).
- FOR GLP-1: REQUIRED - every image brief MUST describe either (1) a GLP-1 medication bottle or pen visible in the scene (e.g. Ozempic, Wegovy, Mounjaro, Zepbound pen or box), OR (2) the text "GLP-1" or a medication name visible on a label, screen, or document. Use VARIETY: product-only (bottle/pen, screen), quiz interfaces, document/surface, or with people when the concept calls for it. People optional.
- Visuals and concepts can be invented and need not match niche stereotypes; aim for hyperrealistic, diverse representation (any age, demographic, psychographic).
=== PSYCHOLOGICAL PRINCIPLES ===
- Loss Aversion: Make them feel what they're losing/missing
- Specificity = Believability: Specific numbers beat round numbers
- Identity Targeting: Direct demographic callouts create self-selection
- Curiosity Gap: "THIS" and "Instead" demand click to close loop
- Social Proof: "Thousands are doing X" triggers herd behavior
- Native Disguise: Content that doesn't look like an ad bypasses filters
=== CRITICAL RULES ===
1. Use the niche as inspiration only—you may invent or blend angles, concepts, triggers, and audiences from any domain for maximum diversity.
2. Use SPECIFIC numbers from the numbers section above when they fit; invent or adapt when it strengthens the hook.
3. ALWAYS create curiosity gap with "THIS", "Instead", "After", "Secret"; use any clickbait or bold phrasing that works.
4. NEVER look like an ad - look like NEWS, PROOF, or UGC
5. Use ACCUSATION framing for maximum impact
6. The image MUST match the {framework_data.get('name', framework)} framework style
=== OUTPUT FORMAT (JSON) ===
{{
"headline": "<10 words, pattern interrupt>",
"primary_text": "<2-3 emotional sentences>",
"description": "<one sentence, max 10 words>",
"body_story": "<8-12 sentence story: relatable pain, tension, transformation, hope, soft CTA; first/second person>",
"image_brief": "<scene following concept and {framework_data.get('name', framework)} style; organic feel>",
"cta": "{cta}",
"psychological_angle": "<angle name or primary trigger>",
"why_it_works": "<brief mechanism>"
}}
Generate the ad copy now. Organic content, immediate emotional response."""
return prompt
# --- Image prompt ---
def _build_image_prompt(
self,
niche: str,
ad_copy: Dict[str, Any],
visual_styles: List[str],
visual_mood: str,
camera_angle: str,
lighting: str,
composition: str,
visual_style_data: Optional[Dict[str, Any]] = None,
trending_context: Optional[str] = None,
) -> str:
"""
Build professional image generation prompt.
Uses detailed specifications, style guidance, and negative prompts.
Creates AUTHENTIC, ORGANIC CONTENT aesthetic.
Text (if included) should be part of the natural scene, NOT an overlay.
When trending_context is set, mood and atmosphere align with the current occasion.
"""
image_brief = ad_copy.get("image_brief", "")
headline = ad_copy.get("headline", "")
psychological_angle = ad_copy.get("psychological_angle", "")
# GLP-1: optionally use spreadsheet-style image creative prompts (Creative ad images sheet)
visual_scene_description = image_brief
if niche == "glp1":
niche_data_img = self._get_niche_data(niche)
creative_prompts = niche_data_img.get("image_creative_prompts") or []
if creative_prompts and random.random() < 0.5:
chosen = random.choice(creative_prompts)
visual_scene_description = chosen.get("image_prompt", image_brief)
# Same framework used for copy is used for visual (no separate container)
framework_key = ad_copy.get("framework_key") or ad_copy.get("container_key", "")
framework_name = ad_copy.get("framework") or ad_copy.get("container_used", "Standard Ad")
price_anchor = ad_copy.get("price_anchor", "$97")
framework_data_img = get_framework(framework_key) if framework_key else None
if not framework_data_img:
framework_data_img = {"name": framework_name, "visual_style": "Standard ad format"}
# Select visual style (use visuals.py data if available, otherwise strategy visuals)
if visual_style_data and isinstance(visual_style_data, dict):
visual_style = visual_style_data.get("prompt_guidance", "")
visual_style_name = visual_style_data.get("name", "")
else:
visual_style = random.choice(visual_styles) if visual_styles else ""
visual_style_name = ""
# Randomly decide which elements to include (for variety)
include_vintage_effects = random.random() < 0.7 # 70% chance
include_text_overlay = random.random() < 0.8 # 80% chance (headline on image)
include_framework_format = random.random() < 0.4 # 40% chance (many images clean without framework UI style)
# Select vintage film style and damage effects (only if including vintage)
vintage_style = random.choice(VINTAGE_FILM_STYLES) if include_vintage_effects else None
film_damage = random.choice(FILM_DAMAGE_EFFECTS) if include_vintage_effects else None
# Get color palette from visuals.py based on trigger/mood
color_palette = get_color_palette(visual_mood.lower().replace(" ", "_").replace("-", "_"))
# Auto insurance ad-format graphics: allow headline + prices/rates/CTA as part of layout
is_auto_insurance_ad_format = niche == "auto_insurance"
# Text styling options for variety - natural text in scene (NO overlays/banners)
text_positions = [
"naturally integrated into the scene",
"as part of a document or sign in the image",
"on a surface within the scene (wall, paper, etc.)",
"as natural text element in the environment",
"integrated into the scene naturally",
]
text_position = random.choice(text_positions)
text_colors = [
"natural text color that fits the scene",
"text that appears naturally in the environment",
"text that looks like it belongs in the scene",
"authentic text appearance, not overlaid",
"text as part of the natural scene elements",
"realistic text that fits the environment",
]
text_color = random.choice(text_colors)
# Niche-specific image guidance (for auto_insurance: no forced subjects/props; for GLP-1: bottle or name required)
if niche == "auto_insurance":
niche_data = self._get_niche_data(niche)
niche_image_guidance = (niche_data.get("image_guidance", "") + """
PEOPLE, FACES, AND CARS ARE OPTIONAL. Only include them when the VISUAL SCENE description explicitly mentions them. Most ad formats are typography, layout, and buttons only.
NO fake or made-up brand/company names (no gibberish); use generic text only or omit. NO in-car dashboard mockups or screens inside car interiors; stick to the 6 defined ad formats only (official notification, social post, coverage tiers, car brand grid, gift card CTA, savings/urgency)."""
)
elif niche == "glp1":
niche_data = self._get_niche_data(niche)
niche_image_guidance = (niche_data.get("image_guidance", "") + """
CRITICAL - GLP-1 PRODUCT VISIBILITY: The image MUST show at least one of: (1) A GLP-1 medication bottle or injectable pen (e.g. Ozempic, Wegovy, Mounjaro, Zepbound) in the scene, OR (2) The text "GLP-1" or a medication name (Ozempic, Wegovy, Mounjaro, etc.) visible on a label, screen, document, or surface. Do not generate a GLP-1 ad image without the product or name visible."""
)
else:
niche_data = self._get_niche_data(niche)
niche_image_guidance = niche_data.get("image_guidance", "")
# Framework visual guidance (same framework as copy)
framework_visual_guidance = get_framework_visual_guidance(framework_key) if framework_key else (framework_data_img.get("visual_style") or framework_data_img.get("visual_guidance", ""))
is_chat_style = "chat_style" in framework_data_img.get("tags", [])
is_document_style = "document_style" in framework_data_img.get("tags", [])
framework_format_section = f"""
=== FRAMEWORK FORMAT REQUIREMENTS ===
CRITICAL: Use ONLY the {framework_data_img.get('name', 'Standard Ad')} framework style. DO NOT mix multiple formats.
Framework: {framework_data_img.get('name', 'Standard Ad')}
Visual Guidance: {framework_visual_guidance}
REQUIREMENTS:
1. USE ONLY THIS FRAMEWORK STYLE - NO mixing (e.g. no WhatsApp + memo, no iMessage + document)
2. NO decorative borders, frames, or boxes
3. NO banners, badges, or logos
4. NO overlay boxes or rectangular overlays
5. Focus on authentic, natural appearance of the {framework_data_img.get('name', 'Standard Ad')} format only
{"=== TEXT REQUIREMENTS FOR CHAT-STYLE FRAMEWORKS ===" if is_chat_style else ""}
{"CRITICAL: All text in chat bubbles MUST be:" if is_chat_style else ""}
{"- READABLE and COHERENT (not gibberish, not placeholder text)" if is_chat_style else ""}
{f"- Realistic conversation text related to {niche.replace('_', ' ').title()}" if is_chat_style else ""}
{"- Proper spelling and grammar" if is_chat_style else ""}
{"- Natural message flow (2-4 messages max)" if is_chat_style else ""}
{"- Use the headline or a variation as one of the messages" if is_chat_style else ""}
{"- NO placeholder text like 'lorem ipsum' or random characters" if is_chat_style else ""}
{"=== TEXT REQUIREMENTS FOR DOCUMENT-STYLE FRAMEWORKS ===" if is_document_style else ""}
{"CRITICAL: All text in documents MUST be:" if is_document_style else ""}
{"- READABLE and COHERENT" if is_document_style else ""}
{f"- Related to {niche.replace('_', ' ').title()} topic" if is_document_style else ""}
{"- Proper formatting (title, body text, etc.)" if is_document_style else ""}
{"- NO gibberish or placeholder text" if is_document_style else ""}
"""
# Build flexible prompt based on what to include
vintage_section = ""
if include_vintage_effects and vintage_style and film_damage:
vintage_section = f"""
=== VINTAGE FILM AESTHETIC (OPTIONAL - apply if it fits the style) ===
- {vintage_style}
- Film damage: {film_damage}
- Warm, faded colors
- Visible grain throughout
"""
framework_section = ""
if include_framework_format:
# 40% of images use the same framework's visual style (e.g. iMessage, Reddit)
framework_section = f"""
{framework_format_section}
CRITICAL REMINDERS:
- Use ONLY {framework_data_img.get('name', 'Standard Ad')} format - NO mixing with other formats
- If chat-style framework: All text MUST be readable, coherent, and related to the {niche.replace('_', ' ').title()} niche
- If document-style framework: All text MUST be readable and properly formatted
- NO gibberish, placeholder text, or random characters
- NO decorative borders, frames, or boxes
"""
else:
# 60% of images: clean, natural (no app/screenshot style)
framework_section = """
=== STYLE GUIDANCE (NO FRAMEWORK UI STYLE) ===
- Natural, authentic image - no app/screenshot style
- Must NOT look like a polished advertisement
- Should feel like authentic, organic content
- Real, unpolished, natural appearance
- NO decorative borders, banners, overlays, or boxes
- NO native app interfaces or screenshot-style frames
- Just a clean, natural photograph or scene
"""
text_overlay_section = ""
if include_text_overlay:
text_overlay_section = f"""
=== HEADLINE TEXT (natural text in scene, NOT overlay) ===
IMPORTANT: Include this text ONLY ONCE in the image, as part of the natural scene.
Text to include: "{headline}"
CRITICAL TEXT RULES:
- Include the text ONLY ONCE - do NOT repeat or duplicate it anywhere
- Text should appear in ONE location only (on a document, sign, or surface in the scene)
- Position: {text_position}
- Style: {text_color}
- Ensure readability and correct spelling
- Text must be part of the scene, NOT an overlay on top
- NO decorative borders, boxes, or frames around text
- NO banners, badges, or logos
- Do NOT show the same message in multiple formats or locations
- If text contains numbers/prices, show them ONCE only
"""
else:
text_overlay_section = """
=== NO TEXT ===
Do NOT include any text on this image. Focus on the visual scene only.
NO text overlays, decorative elements, borders, banners, or overlays.
"""
# Build people/faces section outside f-string (f-string expression cannot contain backslash)
if is_auto_insurance_ad_format:
people_faces_section = """=== PEOPLE, FACES, CARS: OPTIONAL ===
Only include people, faces, or vehicles if the VISUAL SCENE description specifically mentions them. Most auto insurance ad formats are typography and layout only - no people or cars needed."""
else:
people_faces_section = """=== PEOPLE AND FACES: OPTIONAL ===
Only include people or faces if the VISUAL SCENE explicitly describes them. Many creatives work without people—product shots, documents, layouts, objects, text-only are all valid. If this image does include people or faces, they MUST look like real, original people with:
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
- Natural facial asymmetry (no perfectly symmetrical faces)
- Unique, individual facial features (not generic or model-like)
- Natural expressions with authentic micro-expressions
- Realistic skin tones with natural variations and undertones
- Natural hair texture with individual strands
- Faces that look like real photographs of real people, NOT AI-generated portraits
- Avoid any faces that look synthetic, fake, or obviously computer-generated"""
# Build authenticity section outside f-string (f-string expression cannot contain backslash)
if is_auto_insurance_ad_format:
authenticity_section = """=== AUTHENTICITY (only if image includes people/cars per VISUAL SCENE) ===
If you included people or vehicles, they should look realistic. Otherwise focus on layout and typography only."""
else:
authenticity_section = """=== AUTHENTICITY REQUIREMENTS ===
Creatives may have no people (product-only, document, layout, objects, text). PEOPLE (only if present):
- Real, relatable individuals in everyday clothing
- Any hyperrealistic audience—any age, demographic, or psychographic; niche is a suggestion, not a rule
- Natural expressions and trustworthy energy
Visuals and concepts can be invented and need not match niche stereotypes; aim for hyperrealistic, diverse representation.
FACES (close-up):
- Photorealistic texture with visible pores and natural variation
- Subtle asymmetry and unique features—never plastic or model-perfect
- Believable micro-expressions with natural lighting and tone shifts
SETTINGS (if present):
- Real, lived-in locations with everyday props and a touch of natural clutter
DOCUMENTS (if present):
- Realistic bills or statements with legible details, highlights, and gentle wear"""
prompt = f"""Create a Facebook advertisement image that looks like AUTHENTIC, ORGANIC CONTENT.
If the image looks like it belongs on a stock website, it has failed.
{vintage_section}
{framework_section}
{text_overlay_section}
=== VISUAL SCENE ===
{visual_scene_description}
{"=== AUTO INSURANCE AD GRAPHIC (ONLY these 6 creative types) ===" if is_auto_insurance_ad_format else ""}
{"Follow the VISUAL SCENE description exactly. Use ONLY these 6 formats: (1) official notification, (2) social post card, (3) coverage tier panels, (4) car brand grid, (5) gift card CTA, (6) savings/urgency. No other creative types. Include headline, prices, rates, and CTA or button text as specified. Do NOT add people, faces, or cars unless the VISUAL SCENE explicitly asks for them. Do NOT create in-car dashboard mockups, screens inside car interiors, or headshots on displays. Do NOT include fake or made-up brand/company names; use generic text only (e.g. Compare Providers, See Rates) or omit. Render as a clean, modern ad graphic with clear typography and layout." if is_auto_insurance_ad_format else ""}
{people_faces_section}
{"- For this ad graphic layout, headline and price/rate text are part of the design; include them as specified in VISUAL SCENE." if is_auto_insurance_ad_format else "IMPORTANT: Do NOT display numbers, prices, dollar amounts, or savings figures in the image unless they naturally appear as part of the scene (e.g. on a document or sign). Focus on the visual scene (people optional; product/layout-only is fine). Numbers should be in the ad copy, not the image."}
=== VISUAL SPECIFICATIONS ===
STYLE: {visual_style} - {"clean modern ad graphic, professional layout" if is_auto_insurance_ad_format else "rendered in vintage documentary aesthetic"}
MOOD: {visual_mood} - {"trustworthy, clear, high-contrast" if is_auto_insurance_ad_format else "nostalgic, authentic, trustworthy"}
CAMERA: {camera_angle} - documentary/candid feel
LIGHTING: {lighting} - natural, not studio-polished
COMPOSITION: {composition}
{get_trending_image_guidance(trending_context)}
{niche_image_guidance}
{authenticity_section}
=== NEGATIVE PROMPTS (AVOID) ===
- No polished studio lighting or stock-photo aesthetics
- No synthetic, plastic, or perfectly symmetrical faces
- No decorative frames, overlays, badges, or repeated text
- No gibberish, placeholder strings, or fake brand/company names{" (keep labels generic and only where the layout specifies)" if is_auto_insurance_ad_format else ""}
- No mixing multiple framework formats or duplicating the same message
{"- No in-car dashboard mockups, interior screens, or faces on displays" if is_auto_insurance_ad_format else ""}
{"- Numbers, prices, or dollar amounts should appear only when they naturally belong in the scene" if not is_auto_insurance_ad_format else "- Keep typography limited to the headline, rates, and CTA exactly as described"}
=== OUTPUT ===
{"Create a scroll-stopping auto insurance ad graphic. Follow the VISUAL SCENE layout exactly: headline, rates/prices, and CTA or buttons as specified. Use only the 6 defined formats (official notification, social post, coverage tiers, car brand grid, gift card CTA, savings/urgency). No other creative types. No fake brand names; no in-car dashboard or screen mockups; no headshots on displays. Clean typography and layout only." if is_auto_insurance_ad_format else f"Create a scroll-stopping image that feels authentic and organic. {'Include the headline text ONCE as specified above - do NOT duplicate it.' if include_text_overlay else 'Focus on the visual scene without text.'} The image should feel like real content - NOT like a designed advertisement."}
CRITICAL REQUIREMENTS:
{"- Follow the VISUAL SCENE layout exactly; use borders, buttons, and rate cards only where described." if is_auto_insurance_ad_format else "- Keep the scene natural—no added frames, overlays, or decorative borders"}
- Place text or CTA elements exactly once in the location described
- Present the core message once; do not repeat it elsewhere
{"- Maintain clean typography and composition per VISUAL SCENE." if is_auto_insurance_ad_format else "- Focus on the authentic moment, not a polished ad layout"}"""
# Refine and clean the prompt before sending (pass niche for demographic fixes)
refined_prompt = self._refine_image_prompt(prompt, niche=niche)
return refined_prompt
def _refine_image_prompt(self, prompt: str, niche: str = None) -> str:
"""
Refine and clean the image prompt for affiliate marketing creatives.
Fixes illogical elements:
- Contradictory instructions
- Wrong demographics for niche
- Unrealistic visual combinations
- Corporate/stock photo aesthetics (bad for affiliate marketing)
- Meta-instructions that confuse image models
Affiliate marketing principle: Low-production, authentic images outperform polished studio shots.
"""
import re
# =====================================================================
# 1. REMOVE META-INSTRUCTIONS (confuse image models)
# =====================================================================
meta_patterns = [
r'\(for model, not to display\)',
r'\(apply these, don\'t display\)',
r'\(for reference only\)',
r'\(internal use\)',
r'IMPORTANT: Display ONLY',
r'IMPORTANT: If including',
r'IMPORTANT: Use this',
r'IMPORTANT: Follow this',
r'CRITICAL: Do not',
r'NOTE: This is for',
]
for pattern in meta_patterns:
prompt = re.sub(pattern, '', prompt, flags=re.IGNORECASE)
# =====================================================================
# 2. FIX DEMOGRAPHIC ISSUES (niche-specific from niche data)
# Skip niche demographic overrides when the prompt already describes a specific
# audience (unrestricted mode: allow diverse/invented audiences to stand).
# =====================================================================
prompt_lower = prompt.lower()
has_explicit_audience = any(
phrase in prompt_lower
for phrase in [
"aged ", "years old", "year old", "woman", "man", "demographic",
"hyperrealistic", "diverse representation", "any age",
]
)
if not has_explicit_audience:
niche_data_sanitize = self._get_niche_data(niche) if niche else {}
for pattern, replacement in niche_data_sanitize.get("prompt_sanitization_replacements", []):
prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
# =====================================================================
# 3. FIX ILLOGICAL VISUAL COMBINATIONS
# =====================================================================
# Can't have both "no text" and "headline text"
if 'no text' in prompt_lower and ('headline' in prompt_lower or 'text overlay' in prompt_lower):
# Keep "no text" instruction, remove headline sections
prompt = re.sub(
r'===.*HEADLINE.*===[\s\S]*?(?====|$)',
'',
prompt,
flags=re.IGNORECASE | re.MULTILINE
)
prompt = re.sub(r'headline[:\s]+["\']?[^"\']+["\']?', '', prompt, flags=re.IGNORECASE)
# Can't have both "minimalist" and "cluttered/busy"
if 'minimalist' in prompt_lower and any(word in prompt_lower for word in ['cluttered', 'busy', 'crowded', 'chaotic']):
prompt = re.sub(r'\b(cluttered|busy|crowded|chaotic)\b', 'clean', prompt, flags=re.IGNORECASE)
# Can't have both "dark/moody" and "bright/cheerful"
if 'dark' in prompt_lower and 'moody' in prompt_lower:
prompt = re.sub(r'\b(bright|cheerful|sunny|vibrant)\b', 'subtle', prompt, flags=re.IGNORECASE)
elif 'bright' in prompt_lower and 'cheerful' in prompt_lower:
prompt = re.sub(r'\b(dark|moody|dramatic|gloomy)\b', 'soft', prompt, flags=re.IGNORECASE)
# Can't have "indoor" scene with "outdoor" elements
if 'indoor' in prompt_lower or 'inside' in prompt_lower:
if 'sunlight streaming' not in prompt_lower: # Window light is okay
prompt = re.sub(r'\b(outdoor|outside|garden|yard|street)\b(?! view)', 'indoor', prompt, flags=re.IGNORECASE)
# =====================================================================
# 4. FIX AFFILIATE MARKETING ANTI-PATTERNS
# =====================================================================
# Remove corporate/stock photo aesthetics (bad for affiliate marketing)
stock_photo_terms = [
(r'\bstock photo\b', 'authentic photo'),
(r'\bprofessional studio\b', 'natural setting'),
(r'\bperfect lighting\b', 'natural lighting'),
(r'\bcorporate headshot\b', 'candid portrait'),
(r'\bpolished commercial\b', 'authentic UGC-style'),
(r'\bgeneric model\b', 'real person'),
(r'\bshutterstock style\b', 'authentic casual'),
(r'\bistock style\b', 'documentary style'),
]
for pattern, replacement in stock_photo_terms:
prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
# Ensure authenticity markers for affiliate marketing
if 'ugc' in prompt_lower or 'authentic' in prompt_lower:
# Remove conflicting professional markers
prompt = re.sub(r'\b(studio backdrop|professional lighting setup|commercial shoot)\b',
'natural environment', prompt, flags=re.IGNORECASE)
# =====================================================================
# 5. SOFTEN EXTREME BODY LANGUAGE ONLY (allow diverse/invented visuals)
# =====================================================================
# Only replace truly extreme/harmful terms; allow dramatic transformation etc.
unrealistic_patterns = [
(r'\b(impossibly thin|skeletal|anorexic)\b', 'healthy fit'),
]
for pattern, replacement in unrealistic_patterns:
prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
# =====================================================================
# 6. CLEAN UP FORMATTING AND STRUCTURE
# =====================================================================
# Remove developer instructions
lines = prompt.split('\n')
cleaned_lines = []
for line in lines:
line_lower = line.lower()
# Skip developer-only lines
if any(phrase in line_lower for phrase in [
'do not display', 'not to be displayed', 'for debugging',
'metadata:', 'internal:', 'developer note'
]):
continue
# Skip empty placeholder content
if any(phrase in line_lower for phrase in ['n/a', 'not provided', 'see above', 'tbd']):
if len(line.strip()) < 20: # Short placeholder lines
continue
cleaned_lines.append(line)
prompt = '\n'.join(cleaned_lines)
# Simplify emphatic markers (image models don't need shouting)
prompt = re.sub(r'\bCRITICAL:\s*', '', prompt, flags=re.IGNORECASE)
prompt = re.sub(r'\bIMPORTANT:\s*', '', prompt, flags=re.IGNORECASE)
prompt = re.sub(r'\bNOTE:\s*', '', prompt, flags=re.IGNORECASE)
prompt = re.sub(r'\bMUST:\s*', '', prompt, flags=re.IGNORECASE)
# Remove excessive blank lines
prompt = re.sub(r'\n{3,}', '\n\n', prompt)
# Remove empty sections
prompt = re.sub(r'===\s*===', '', prompt)
prompt = re.sub(r'===\s*\n\s*===', '===', prompt)
# =====================================================================
# 7. ENSURE LOGICAL COHERENCE
# =====================================================================
# Remove duplicate prohibitions
seen_prohibitions = set()
final_lines = []
for line in prompt.split('\n'):
line_lower = line.lower().strip()
if line_lower.startswith('- no ') or line_lower.startswith('no '):
prohibition_key = re.sub(r'[^a-z\s]', '', line_lower)[:50]
if prohibition_key in seen_prohibitions:
continue
seen_prohibitions.add(prohibition_key)
final_lines.append(line)
prompt = '\n'.join(final_lines)
# =====================================================================
# 8. ADD AFFILIATE MARKETING QUALITY MARKERS
# =====================================================================
# Ensure the prompt has authenticity markers if not present
if 'authentic' not in prompt_lower and 'ugc' not in prompt_lower and 'real' not in prompt_lower:
prompt += "\n\nStyle: Authentic; can be real-person, product-only, or layout-only. Not overly polished or corporate."
# Final trim
prompt = prompt.strip()
# Ensure prompt doesn't end mid-sentence
if prompt and not prompt[-1] in '.!?"\'':
prompt += '.'
return prompt
# --- Public: single ad (standard) ---
async def generate_ad(
self,
niche: str,
num_images: int = 1,
image_model: Optional[str] = None,
username: Optional[str] = None, # Username of the user generating the ad
target_audience: Optional[str] = None,
offer: Optional[str] = None,
use_trending: bool = False, # Whether to incorporate trending topics
trending_context: Optional[str] = None, # Optional specific trending context
) -> Dict[str, Any]:
"""
Generate a complete ad creative with copy and image.
Uses maximum randomization to ensure different results every time:
- Random strategy selection (2-3 from pool)
- Random hook selection
- Random visual style
- Random creative direction
- Random visual mood
- Random camera angle, lighting, composition
- Random seed for image generation
Can optionally incorporate current trending topics from Google News.
Args:
niche: Target niche (home_insurance or glp1)
num_images: Number of images to generate
use_trending: Whether to incorporate current trending topics
trending_context: Specific trending context (auto-fetched if not provided)
Returns:
Dict with ad copy, image path, and metadata
"""
# Load niche data
niche_data = self._get_niche_data(niche)
# Get framework first (needed for compatibility scoring)
all_frameworks = get_all_frameworks()
framework_keys = list(all_frameworks.keys())
# 70% chance to use niche-preferred, 30% chance for any framework (ensures all are used)
if random.random() < 0.7:
framework_data = get_frameworks_for_niche(niche, count=1)[0]
else:
# Use any framework for maximum variety
framework_key = random.choice(framework_keys)
framework_data = {"key": framework_key, **all_frameworks[framework_key]}
framework = framework_data["name"]
framework_key = framework_data["key"]
# IMPROVEMENT: Select compatible strategies based on framework
num_strategies = random.randint(2, 3)
strategies = self._select_compatible_strategies(niche_data, framework_key, count=num_strategies)
hooks = self._random_hooks(strategies, count=3)
# IMPROVEMENT: Use visual library in addition to strategy visuals
visual_styles = self._random_visual_styles(strategies, count=2, niche=niche, use_library=True)
creative_direction = random.choice(niche_data["creative_directions"])
visual_mood = random.choice(niche_data["visual_moods"])
ctas = await self._generate_ctas_async(niche, framework_data.get("name"))
cta = random.choice(ctas) if ctas else "Learn More"
# Use visual elements from visuals.py (instead of hardcoded)
visual_style_data = get_random_visual_style()
camera_angle = get_random_camera_angle()
lighting = get_random_lighting()
composition = get_random_composition()
# Get psychological trigger for additional guidance
trigger_data = get_random_trigger()
trigger_combination = get_trigger_combination()
# Get power words for copy enhancement
power_words = get_power_words(category=None, count=5)
# Get random angle × concept combination (like matrix generation)
combination = matrix_service.generate_single_combination(niche)
angle = combination["angle"]
concept = combination["concept"]
# Fetch trending context if requested
trending_info = None
if use_trending and trend_monitor_available:
try:
if not trending_context:
# Auto-fetch current trends (occasions + news); occasions are date-based (e.g. Valentine's Week)
print("📰 Fetching current trending topics (occasions + news)...")
trends_data = await trend_monitor.get_relevant_trends_for_niche(niche)
if trends_data and trends_data.get("relevant_trends"):
# Use top trend for context
top_trend = trends_data["relevant_trends"][0]
trending_context = f"{top_trend['title']} - {top_trend['summary']}"
trending_info = {
"title": top_trend["title"],
"summary": top_trend["summary"],
"category": top_trend.get("category", "General"),
"source": "Google News",
}
print(f"✓ Using trend: {top_trend['title']}")
else:
# User provided specific trending context
trending_info = {
"context": trending_context,
"source": "User provided",
}
print(f"✓ Using user-provided trending context")
except Exception as e:
print(f"Warning: Failed to fetch trending topics: {e}")
use_trending = False
elif use_trending and not trend_monitor_available:
print("Warning: Trending topics requested but trend monitor not available")
use_trending = False
# Generate ad copy via LLM with professional prompt
copy_prompt = self._build_copy_prompt(
niche=niche,
niche_data=niche_data,
strategies=strategies,
hooks=hooks,
creative_direction=creative_direction,
framework=framework,
framework_data=framework_data,
cta=cta,
trigger_data=trigger_data,
trigger_combination=trigger_combination,
power_words=power_words,
angle=angle,
concept=concept,
target_audience=target_audience,
offer=offer,
trending_context=trending_context if use_trending else None,
)
ad_copy = await llm_service.generate_json(
prompt=copy_prompt,
temperature=0.95, # High for variety
)
# Same framework drives visuals: set framework on ad_copy for image step
ad_copy["framework_key"] = framework_data["key"]
ad_copy["framework"] = framework_data["name"]
ad_copy["container_key"] = framework_data["key"] # backward compat
ad_copy["container_used"] = framework_data["name"] # backward compat
# Generate image(s) with professional prompt - PARALLELIZED
async def generate_single_image(image_index: int):
"""Helper function to generate a single image with all processing."""
# Build image prompt with all parameters (include trending context so images match the occasion)
image_prompt = self._build_image_prompt(
niche=niche,
ad_copy=ad_copy,
visual_styles=visual_styles,
visual_mood=visual_mood,
camera_angle=camera_angle,
lighting=lighting,
composition=composition,
visual_style_data=visual_style_data,
trending_context=trending_context if use_trending else None,
)
# Store the refined prompt for database saving
refined_image_prompt = image_prompt
# Generate with random seed
seed = random.randint(1, 2147483647)
try:
# Generate image (async)
image_bytes, model_used, image_url = await image_service.generate(
prompt=image_prompt,
width=settings.image_width,
height=settings.image_height,
seed=seed,
model_key=image_model,
)
# Generate filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
unique_id = str(uuid.uuid4())[:8]
filename = f"{niche}_{timestamp}_{unique_id}_{image_index}.png"
# Upload to R2 in parallel thread (sync operation)
r2_url = None
if r2_storage_available:
try:
r2_storage = get_r2_storage()
if r2_storage:
# Run R2 upload in thread pool (sync operation)
loop = asyncio.get_event_loop()
r2_url = await loop.run_in_executor(
None,
lambda: r2_storage.upload_image(
image_bytes=image_bytes,
filename=filename,
niche=niche,
)
)
print(f"Image {image_index + 1} uploaded to R2: {r2_url}")
except Exception as e:
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
# Save image locally conditionally (based on environment settings)
filepath = self._save_image_locally(image_bytes, filename)
# Use R2 URL if available, otherwise use Replicate URL, fallback to local
final_image_url = r2_url or image_url
return {
"filename": filename,
"filepath": filepath,
"image_url": final_image_url,
"r2_url": r2_url,
"model_used": model_used,
"seed": seed,
"image_prompt": refined_image_prompt,
}
except Exception as e:
return {
"error": str(e),
"seed": seed,
"image_prompt": refined_image_prompt,
}
# Generate all images in parallel using asyncio.gather
if num_images > 1:
print(f"🔄 Generating {num_images} images in parallel...")
image_tasks = [generate_single_image(i) for i in range(num_images)]
generated_images = await asyncio.gather(*image_tasks)
else:
# Single image - no need for parallelization
generated_images = [await generate_single_image(0)]
# Generate unique ID
ad_id = str(uuid.uuid4())
# Build metadata
metadata = {
"strategies_used": [s["name"] for s in strategies],
"creative_direction": creative_direction,
"visual_mood": visual_mood,
"framework": framework,
"camera_angle": camera_angle,
"lighting": lighting,
"composition": composition,
"hooks_inspiration": hooks,
"visual_styles": visual_styles,
"visual_style": visual_style_data.get("name") if visual_style_data else None,
}
# Save to database (save EACH image as a separate record for gallery visibility)
# Ensure database connection is initialized
if db_service and db_service.collection is None and settings.mongodb_url:
try:
await db_service.connect()
except Exception as e:
print(f"Warning: Could not connect to database: {e}")
# Save each image variation as a separate database record
saved_ad_ids = []
if db_service and db_service.collection is not None and username:
for img_idx, image in enumerate(generated_images):
if image.get("error"):
continue # Skip failed images
try:
db_id = await db_service.save_ad_creative(
niche=niche,
title=None, # No title for standard flow
headline=ad_copy.get("headline", ""),
primary_text=ad_copy.get("primary_text", ""),
description=ad_copy.get("description", ""),
body_story=ad_copy.get("body_story", ""),
cta=ad_copy.get("cta", ""),
psychological_angle=ad_copy.get("psychological_angle", ""),
why_it_works=ad_copy.get("why_it_works", ""),
username=username, # Pass username
image_url=image.get("image_url"),
r2_url=image.get("r2_url"),
image_filename=image.get("filename"),
image_model=image.get("model_used"),
image_seed=image.get("seed"),
image_prompt=image.get("image_prompt"), # Save the final refined prompt
generation_method="standard",
angle_key=angle.get("key"),
angle_name=angle.get("name"),
angle_trigger=angle.get("trigger"),
angle_category=angle.get("category"),
concept_key=concept.get("key"),
concept_name=concept.get("name"),
concept_structure=concept.get("structure"),
concept_visual=concept.get("visual"),
concept_category=concept.get("category"),
metadata={**metadata, "variation_index": img_idx, "total_variations": len(generated_images)},
)
if db_id:
saved_ad_ids.append(db_id)
print(f"✓ Saved ad creative variation {img_idx + 1}/{len(generated_images)} to database: {db_id}")
except Exception as e:
print(f"Warning: Failed to save variation {img_idx + 1} to database: {e}")
# Use first saved ID as the main ad_id
if saved_ad_ids:
ad_id = saved_ad_ids[0]
# Build response
result = {
"id": ad_id,
"niche": niche,
"created_at": datetime.now().isoformat(),
# Ad copy
"headline": ad_copy.get("headline", ""),
"primary_text": ad_copy.get("primary_text", ""),
"description": ad_copy.get("description", ""),
"body_story": ad_copy.get("body_story", ""),
"cta": ad_copy.get("cta", ""),
"psychological_angle": ad_copy.get("psychological_angle", ""),
"why_it_works": ad_copy.get("why_it_works", ""),
# Image(s) with URLs
"images": generated_images,
# Metadata for debugging/learning
"metadata": metadata,
}
return result
# --- Public: matrix ad ---
async def generate_ad_with_matrix(
self,
niche: str,
angle_key: Optional[str] = None,
concept_key: Optional[str] = None,
custom_angle: Optional[str] = None,
custom_concept: Optional[str] = None,
num_images: int = 1,
image_model: Optional[str] = None,
username: Optional[str] = None,
core_motivator: Optional[str] = None,
target_audience: Optional[str] = None,
offer: Optional[str] = None,
) -> Dict[str, Any]:
"""
Generate ad using angle × concept matrix approach.
This provides more systematic ad generation with explicit
control over psychological angle and visual concept.
Args:
niche: Target niche
angle_key: Specific angle key (optional, random if not provided)
concept_key: Specific concept key (optional, random if not provided)
custom_angle: Custom angle dict (used when angle_key is 'custom')
custom_concept: Custom concept dict (used when concept_key is 'custom')
num_images: Number of images to generate
Returns:
Complete ad creative with angle and concept metadata
"""
from data.angles import get_angle_by_key
from data.concepts import get_concept_by_key
# Handle custom or predefined angle
angle = None
if angle_key == "custom" and custom_angle:
# Parse custom angle - it should be a dict with name, trigger, example
if isinstance(custom_angle, str):
# If it's a JSON string, parse it
try:
import json
angle = json.loads(custom_angle)
except:
# If plain text, create a basic structure
angle = {
"key": "custom",
"name": "Custom Angle",
"trigger": "Emotion",
"example": custom_angle,
"category": "Custom",
}
else:
angle = custom_angle
# Ensure required fields
angle["key"] = "custom"
angle["category"] = angle.get("category", "Custom")
elif angle_key:
angle = get_angle_by_key(angle_key)
if not angle:
raise ValueError(f"Invalid angle_key: {angle_key}")
# Handle custom or predefined concept
concept = None
if concept_key == "custom" and custom_concept:
# Parse custom concept - it should be a dict with name, structure, visual
if isinstance(custom_concept, str):
# If it's a JSON string, parse it
try:
import json
concept = json.loads(custom_concept)
except:
# If plain text, create a basic structure
concept = {
"key": "custom",
"name": "Custom Concept",
"structure": custom_concept,
"visual": custom_concept,
"category": "Custom",
}
else:
concept = custom_concept
# Ensure required fields
concept["key"] = "custom"
concept["category"] = concept.get("category", "Custom")
elif concept_key:
concept = get_concept_by_key(concept_key)
if not concept:
raise ValueError(f"Invalid concept_key: {concept_key}")
# If both angle and concept are provided (custom or predefined)
if angle and concept:
combination = {
"angle": angle,
"concept": concept,
"prompt_guidance": f"""
ANGLE: {angle.get('name', 'Custom Angle')}
- Psychological trigger: {angle.get('trigger', 'Emotion')}
- Example: "{angle.get('example', '')}"
CONCEPT: {concept.get('name', 'Custom Concept')}
- Structure: {concept.get('structure', '')}
- Visual: {concept.get('visual', '')}
""",
}
else:
# Fall back to auto-generation
combination = matrix_service.generate_single_combination(niche)
angle = combination["angle"]
concept = combination["concept"]
# Get niche data
niche_data = NICHE_DATA.get(niche, home_insurance.get_niche_data)()
ctas = await self._generate_ctas_async(niche)
cta = random.choice(ctas) if ctas else "Learn More"
# Build specialized prompt using angle + concept
ad_copy_prompt = self._build_matrix_ad_prompt(
niche=niche,
angle=angle,
concept=concept,
niche_data=niche_data,
cta=cta,
core_motivator=core_motivator,
target_audience=target_audience,
offer=offer,
)
# Generate ad copy
response_format = {
"type": "json_schema",
"json_schema": {
"name": "ad_copy",
"schema": {
"type": "object",
"properties": {
"headline": {"type": "string"},
"primary_text": {"type": "string"},
"description": {"type": "string"},
"body_story": {"type": "string"},
"image_brief": {"type": "string"},
"cta": {"type": "string"},
"psychological_angle": {"type": "string"},
"why_it_works": {"type": "string"},
},
"required": ["headline", "primary_text", "description", "body_story", "image_brief", "cta"],
},
},
}
ad_copy_response = await llm_service.generate(
prompt=ad_copy_prompt,
temperature=0.8,
response_format=response_format,
)
import json
ad_copy = json.loads(ad_copy_response)
# Build image prompt using concept's visual guidance
image_prompt = self._build_matrix_image_prompt(
niche=niche,
angle=angle,
concept=concept,
ad_copy=ad_copy,
core_motivator=core_motivator,
)
# Store the refined prompt for database saving (this is the final prompt sent to image service)
refined_image_prompt = image_prompt
# Generate images - PARALLELIZED
async def generate_single_matrix_image(image_index: int):
"""Helper function to generate a single matrix image with all processing."""
seed = random.randint(1, 2**31 - 1)
try:
# Generate image (async)
image_bytes, model_used, image_url = await image_service.generate(
prompt=image_prompt,
model_key=image_model or settings.image_model,
width=1024,
height=1024,
seed=seed,
)
# Generate filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
unique_id = uuid.uuid4().hex[:8]
filename = f"{niche}_{timestamp}_{unique_id}_{image_index}.png"
# Upload to R2 in parallel thread (sync operation)
r2_url = None
if r2_storage_available:
try:
r2_storage = get_r2_storage()
if r2_storage:
# Run R2 upload in thread pool (sync operation)
loop = asyncio.get_event_loop()
r2_url = await loop.run_in_executor(
None,
lambda: r2_storage.upload_image(
image_bytes=image_bytes,
filename=filename,
niche=niche,
)
)
print(f"Matrix image {image_index + 1} uploaded to R2: {r2_url}")
except Exception as e:
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
# Save image locally conditionally (based on environment settings)
filepath = self._save_image_locally(image_bytes, filename)
# Use R2 URL if available, otherwise use Replicate URL
final_image_url = r2_url or image_url
return {
"filename": filename,
"filepath": filepath,
"image_url": final_image_url,
"r2_url": r2_url,
"model_used": model_used,
"seed": seed,
"image_prompt": refined_image_prompt,
"error": None,
}
except Exception as e:
return {
"filename": None,
"filepath": None,
"image_url": None,
"model_used": settings.image_model,
"seed": seed,
"image_prompt": refined_image_prompt if 'refined_image_prompt' in locals() else None,
"error": str(e),
}
# Generate all images in parallel using asyncio.gather
if num_images > 1:
print(f"🔄 Generating {num_images} matrix images in parallel...")
image_tasks = [generate_single_matrix_image(i) for i in range(num_images)]
images = await asyncio.gather(*image_tasks)
else:
# Single image - no need for parallelization
images = [await generate_single_matrix_image(0)]
# Generate unique ID
ad_id = str(uuid.uuid4())
# Save to database (save EACH image as a separate record for gallery visibility)
# Ensure database connection is initialized
if db_service and db_service.collection is None and settings.mongodb_url:
try:
await db_service.connect()
except Exception as e:
print(f"Warning: Could not connect to database: {e}")
# Save each image variation as a separate database record
saved_ad_ids = []
if db_service and db_service.collection is not None and username:
for img_idx, image in enumerate(images):
if image.get("error"):
continue # Skip failed images
try:
db_id = await db_service.save_ad_creative(
niche=niche,
title=None, # No title for matrix flow
headline=ad_copy.get("headline", ""),
primary_text=ad_copy.get("primary_text", ""),
description=ad_copy.get("description", ""),
body_story=ad_copy.get("body_story", ""),
cta=ad_copy.get("cta", ""),
psychological_angle=ad_copy.get("psychological_angle", angle.get("name", "")),
why_it_works=ad_copy.get("why_it_works", ""),
username=username, # Pass username
image_url=image.get("image_url"),
r2_url=image.get("r2_url"),
image_filename=image.get("filename"),
image_model=image.get("model_used"),
image_seed=image.get("seed"),
image_prompt=image.get("image_prompt"), # Save the final refined prompt
angle_key=angle.get("key"),
angle_name=angle.get("name"),
angle_trigger=angle.get("trigger"),
angle_category=angle.get("category"),
concept_key=concept.get("key"),
concept_name=concept.get("name"),
concept_structure=concept.get("structure"),
concept_visual=concept.get("visual"),
concept_category=concept.get("category"),
generation_method="angle_concept_matrix",
metadata={
"generation_method": "angle_concept_matrix",
"variation_index": img_idx,
"total_variations": len(images),
"visual_style": concept.get("name"), # Use concept name as visual style for matrix generation
},
)
if db_id:
saved_ad_ids.append(db_id)
print(f"✓ Saved matrix ad creative variation {img_idx + 1}/{len(images)} to database: {db_id}")
except Exception as e:
print(f"Warning: Failed to save matrix variation {img_idx + 1} to database: {e}")
# Use first saved ID as the main ad_id
if saved_ad_ids:
ad_id = saved_ad_ids[0]
return {
"id": ad_id,
"niche": niche,
"created_at": datetime.now().isoformat(),
"headline": ad_copy.get("headline", ""),
"primary_text": ad_copy.get("primary_text", ""),
"description": ad_copy.get("description", ""),
"body_story": ad_copy.get("body_story", ""),
"cta": ad_copy.get("cta", ""),
"psychological_angle": ad_copy.get("psychological_angle", angle.get("name", "")),
"why_it_works": ad_copy.get("why_it_works", ""),
"images": images,
"matrix": {
"angle": {
"key": angle.get("key"),
"name": angle.get("name"),
"trigger": angle.get("trigger"),
"category": angle.get("category"),
},
"concept": {
"key": concept.get("key"),
"name": concept.get("name"),
"structure": concept.get("structure"),
"visual": concept.get("visual"),
"category": concept.get("category"),
},
},
"metadata": {
"generation_method": "angle_concept_matrix",
},
}
# --- Public: extensive flow ---
async def generate_ad_extensive(
self,
niche: str,
target_audience: Optional[str] = None,
offer: Optional[str] = None,
num_images: int = 1,
image_model: Optional[str] = None,
num_strategies: int = 5,
username: Optional[str] = None, # Username of the user generating the ad
use_creative_inventor: bool = True,
trend_context: Optional[str] = None,
) -> Dict[str, Any]:
"""
Generate ad using extensive flow: (inventor or researcher) → creative director → designer → copywriter.
When use_creative_inventor=True, the system invents new ad angles, concepts, visuals, and
psychological triggers by itself instead of using the fixed researcher step.
Args:
niche: Target niche (home_insurance, glp1, auto_insurance, or custom)
target_audience: Optional target audience description
offer: Optional offer to run
num_images: Number of images to generate per strategy
image_model: Image generation model to use
num_strategies: Number of creative strategies to generate
use_creative_inventor: If True, use Creative Inventor to generate new angles/concepts/visuals/triggers; if False, use researcher
trend_context: Optional trend or occasion context (used when use_creative_inventor=True)
Returns:
Dict with ad copy, images, and metadata
"""
if not third_flow_available:
raise ValueError("Extensive service not available")
# Map known niche keys to display names; custom niches (e.g. from 'others') pass through as-is
niche_map = {
"home_insurance": "Home Insurance",
"glp1": "GLP-1",
"auto_insurance": "Auto Insurance",
}
niche_display = niche_map.get(niche, niche.replace("_", " ").title())
# Provide default offer if not provided; target_audience is optional when using inventor (AI decides audiences)
if not offer:
offer = f"Get the best {niche_display} solution"
audience_for_retrieve = target_audience or f"People interested in {niche_display}"
# Step 1: Invent new angles/concepts/visuals/triggers + hyper-specific audiences (Creative Inventor) or research (Researcher)
target_audiences_per_strategy: Optional[List[str]] = None
if use_creative_inventor:
print("🧠 Step 1: Inventing new ad angles, concepts, visuals, triggers, and hyper-specific audiences...")
researcher_output, target_audiences_per_strategy = await asyncio.to_thread(
third_flow_service.get_essentials_via_inventor,
niche=niche_display,
offer=offer,
n=num_strategies,
target_audience_hint=target_audience if target_audience else None,
trend_context=trend_context,
)
else:
print("🔍 Step 1: Researching psychology triggers, angles, and concepts...")
if not target_audience:
target_audience = audience_for_retrieve
researcher_output = await asyncio.to_thread(
third_flow_service.researcher,
target_audience=target_audience,
offer=offer,
niche=niche_display
)
if not researcher_output:
raise ValueError("Step 1 returned no results (inventor or researcher)")
# Step 2: Retrieve knowledge (in parallel)
print("📚 Step 2: Retrieving marketing knowledge...")
book_knowledge, ads_knowledge = await asyncio.gather(
asyncio.to_thread(
third_flow_service.retrieve_search,
audience_for_retrieve, offer, niche_display
),
asyncio.to_thread(
third_flow_service.retrieve_ads,
audience_for_retrieve, offer, niche_display
)
)
# Step 3: Creative Director (with per-strategy hyper-specific audiences when from inventor)
print(f"🎨 Step 3: Creating {num_strategies} creative strategy/strategies...")
print(f"📋 Parameters: num_strategies={num_strategies}, num_images={num_images}")
creative_director_target = (
"Various hyper-specific audiences (see per-strategy)" if target_audiences_per_strategy else audience_for_retrieve
)
creative_strategies = await asyncio.to_thread(
third_flow_service.creative_director,
researcher_output=researcher_output,
book_knowledge=book_knowledge,
ads_knowledge=ads_knowledge,
target_audience=creative_director_target,
offer=offer,
niche=niche_display,
n=num_strategies,
target_audiences=target_audiences_per_strategy,
)
if not creative_strategies:
raise ValueError("Creative director returned no strategies")
# Limit to requested number of strategies (in case LLM returns more)
creative_strategies = creative_strategies[:num_strategies]
print(f"📊 Using {len(creative_strategies)} strategy/strategies (requested: {num_strategies})")
# Step 4: Process strategies in parallel (designer + copywriter)
print(f"⚡ Step 4: Processing {len(creative_strategies)} strategies in parallel...")
strategy_tasks = [
asyncio.to_thread(
third_flow_service.process_strategy,
strategy,
niche=niche_display,
)
for strategy in creative_strategies
]
strategy_results = await asyncio.gather(*strategy_tasks)
# Step 5: Generate images for each strategy
# Ensure we only process the requested number of strategies
strategies_to_process = min(len(strategy_results), num_strategies)
print(f"🖼️ Step 5: Generating {num_images} image(s) per strategy for {strategies_to_process} strategy/strategies...")
all_results = []
for idx, (prompt, title, body, description) in enumerate(strategy_results[:strategies_to_process]):
if idx >= num_strategies:
print(f"⚠️ Stopping at strategy {idx + 1} (requested {num_strategies} strategies)")
break
if not prompt:
print(f"Warning: Strategy {idx + 1} has no prompt, skipping...")
continue
# Generate images for this strategy (respecting num_images parameter) - PARALLELIZED
print(f" Generating {num_images} image(s) for strategy {idx + 1}/{len(creative_strategies)}...")
# Variation modifiers to ensure different images
variation_modifiers = [
"different camera angle, unique composition",
"alternative perspective, varied lighting",
"distinct framing, different visual style",
"unique viewpoint, varied composition",
"alternative angle, different mood",
]
# Title/headline to show in image (from copywriter; use strategy title ideas if needed)
text_for_image = (title or creative_strategies[idx].titleIdeas or "").strip() if idx < len(creative_strategies) else (title or "").strip()
text_instruction = ""
if text_for_image:
text_instruction = f'\n\n=== TEXT THAT MUST APPEAR IN THE IMAGE ===\nInclude this text visibly and readably in the image (e.g. on a sign, document, phone screen, poster, or surface): "{text_for_image}"\n- Spell it correctly; make it clearly readable.\n- Include it once as part of the natural scene, not as a separate overlay.'
async def generate_single_extensive_image(img_idx: int):
"""Helper function to generate a single extensive image with all processing."""
try:
# Add text instruction then low quality camera instruction so the image contains the headline/title
prompt_with_text_and_camera = f"{prompt}{text_instruction}\n\n=== CAMERA QUALITY ===\n- The image should look like it was shot from a low quality camera\n- Include characteristics of low quality camera: slight grain, reduced sharpness, lower resolution appearance, authentic camera imperfections\n- Should have the authentic feel of a real photo taken with a basic or older camera device"
# Refine prompt and add variation for each image (pass niche for demographic fixes)
base_refined_prompt = self._refine_image_prompt(prompt_with_text_and_camera, niche=niche)
# Add variation modifier if generating multiple images
if num_images > 1:
variation = variation_modifiers[img_idx % len(variation_modifiers)]
refined_prompt = f"{base_refined_prompt}, {variation}"
else:
refined_prompt = base_refined_prompt
# Generate unique seed for each image
actual_seed = random.randint(1, 2147483647)
# Generate image (async)
image_bytes, model_used, image_url = await image_service.generate(
prompt=refined_prompt,
seed=actual_seed,
model_key=image_model,
)
if not image_bytes:
print(f"Warning: Failed to generate image {img_idx + 1} for strategy {idx + 1}")
return {
"error": "Image generation returned no image data",
"seed": actual_seed,
"image_prompt": refined_prompt,
}
# Generate filename with unique identifier
filename = f"{niche}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}_{img_idx}.png"
# Upload to R2 in parallel thread (sync operation)
r2_url = None
if r2_storage_available:
try:
r2_storage = get_r2_storage()
if r2_storage:
# Run R2 upload in thread pool (sync operation)
loop = asyncio.get_event_loop()
r2_url = await loop.run_in_executor(
None,
lambda: r2_storage.upload_image(
image_bytes, filename=filename, niche=niche
)
)
print(f"Extensive image {img_idx + 1} uploaded to R2: {r2_url}")
except Exception as r2_e:
print(f"Warning: Failed to upload image to R2: {r2_e}")
# Save image locally conditionally (based on environment settings)
filepath = self._save_image_locally(image_bytes, filename)
# Use R2 URL if available, otherwise use Replicate URL
final_image_url = r2_url or image_url
result = {
"filename": filename,
"filepath": filepath,
"image_url": final_image_url,
"r2_url": r2_url,
"model_used": model_used,
"seed": actual_seed,
"image_prompt": refined_prompt,
}
print(f" ✓ Image {img_idx + 1}/{num_images} generated with seed {actual_seed}")
return result
except Exception as e:
print(f"Error generating image {img_idx + 1} for strategy {idx + 1}: {e}")
error_seed = random.randint(1, 2147483647)
return {
"error": str(e),
"seed": error_seed,
"image_prompt": refined_prompt if 'refined_prompt' in locals() else None,
}
# Generate all images in parallel using asyncio.gather
if num_images > 1:
print(f" 🔄 Generating {num_images} images in parallel for strategy {idx + 1}...")
image_tasks = [generate_single_extensive_image(img_idx) for img_idx in range(num_images)]
generated_images = await asyncio.gather(*image_tasks)
else:
# Single image - no need for parallelization
generated_images = [await generate_single_extensive_image(0)]
if not generated_images:
print(f"Warning: No images generated for strategy {idx + 1}, skipping...")
continue
# Build ad copy
strategy = creative_strategies[idx]
headline = title or strategy.titleIdeas or "Check this out"
primary_text = body or strategy.bodyIdeas or ""
description_text = description or strategy.captionIdeas or ""
cta = strategy.cta or "Learn More"
# Save to database (save EACH image as a separate record for gallery visibility)
ad_id = str(uuid.uuid4())
saved_ad_ids = []
if db_service and username:
for img_idx, image in enumerate(generated_images):
if image.get("error"):
continue # Skip failed images
try:
db_id = await db_service.save_ad_creative(
niche=niche,
title=title or "",
headline=headline,
primary_text=primary_text,
description=description_text,
body_story=primary_text, # Use primary_text as body_story
cta=cta,
psychological_angle=strategy.phsychologyTrigger or "",
why_it_works=f"Angle: {strategy.angle}, Concept: {strategy.concept}",
username=username, # Pass username
image_url=image.get("image_url"),
r2_url=image.get("r2_url"),
image_filename=image.get("filename"),
image_model=image.get("model_used"),
image_prompt=image.get("image_prompt"),
generation_method="extensive",
metadata={
"generation_method": "extensive",
"target_audience": target_audience,
"offer": offer,
"strategy_index": idx,
"psychology_trigger": strategy.phsychologyTrigger,
"angle": strategy.angle,
"concept": strategy.concept,
"visual_direction": strategy.visualDirection,
"variation_index": img_idx,
"total_variations": len(generated_images),
},
)
if db_id:
saved_ad_ids.append(db_id)
print(f"✓ Saved extensive ad creative variation {img_idx + 1}/{len(generated_images)} to database: {db_id}")
except Exception as e:
print(f"Warning: Failed to save extensive variation {img_idx + 1} to database: {e}")
# Use first saved ID as the main ad_id
if saved_ad_ids:
ad_id = saved_ad_ids[0]
all_results.append({
"id": ad_id,
"niche": niche,
"created_at": datetime.now().isoformat(),
"title": title or "",
"headline": headline,
"primary_text": primary_text,
"description": description_text,
"body_story": primary_text,
"cta": cta,
"psychological_angle": strategy.phsychologyTrigger or "",
"why_it_works": f"Angle: {strategy.angle}, Concept: {strategy.concept}",
"images": generated_images, # Include ALL images for this strategy
"metadata": {
"strategies_used": [strategy.phsychologyTrigger] if strategy.phsychologyTrigger else ["extensive"],
"creative_direction": f"Extensive: {strategy.angle} × {strategy.concept}",
"visual_mood": strategy.visualDirection.split(".")[0] if strategy.visualDirection else "authentic",
"framework": None,
"camera_angle": None,
"lighting": None,
"composition": None,
"hooks_inspiration": [strategy.titleIdeas] if strategy.titleIdeas else [],
"visual_styles": [strategy.concept] if strategy.concept else [],
},
})
print(f"✓ Strategy {idx + 1} completed with {len(generated_images)} image(s)")
# Return all results (like batch generation)
if all_results:
print(f"📤 Returning {len(all_results)} strategy result(s)")
return all_results
else:
raise ValueError("No ads generated from extensive")
# ========================================================================
# CUSTOM ANGLE/CONCEPT REFINEMENT
# ========================================================================
# --- Refine custom angle/concept ---
async def refine_custom_angle_or_concept(
self,
text: str,
type: str, # "angle" or "concept"
niche: str,
goal: Optional[str] = None,
) -> Dict[str, Any]:
"""
Refine a custom angle or concept text using AI.
Takes raw user input and structures it properly according to
the angle/concept framework used in ad generation.
Args:
text: Raw user input text
type: "angle" or "concept"
niche: Target niche for context
goal: Optional user goal or context
Returns:
Structured angle or concept dict
"""
import json
if type == "angle":
prompt = f"""You are an expert in direct-response advertising psychology.
The user wants to create a custom marketing ANGLE for {niche.replace('_', ' ')} ads.
An ANGLE answers "WHY should I care?" - it's the psychological hook that makes someone stop scrolling.
User's custom angle idea:
"{text}"
{f'User goal/context: {goal}' if goal else ''}
Example format: Name (2-4 words), Trigger (e.g. Fear, Greed, Relief, FOMO), Example hook (5-10 words). e.g. "Save Money" / Greed / "Save $600/year".
Structure the user's idea into a proper angle format.
Return JSON:
{{
"name": "Short descriptive name (2-4 words)",
"trigger": "Primary psychological trigger (e.g., Fear, Hope, Pride, Greed, Relief, FOMO, Curiosity, Anger, Trust)",
"example": "A compelling example hook using this angle (5-10 words)"
}}"""
response_format = {
"type": "json_schema",
"json_schema": {
"name": "refined_angle",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"trigger": {"type": "string"},
"example": {"type": "string"},
},
"required": ["name", "trigger", "example"],
},
},
}
response = await llm_service.generate(
prompt=prompt,
temperature=0.7,
response_format=response_format,
)
result = json.loads(response)
result["key"] = "custom"
result["category"] = "Custom"
result["original_text"] = text
return result
else: # concept
prompt = f"""You are an expert in visual advertising and creative direction.
The user wants to create a custom visual CONCEPT for {niche.replace('_', ' ')} ads.
A CONCEPT answers "HOW do we show it?" - it's the visual approach and structure of the ad.
User's custom concept idea:
"{text}"
{f'User goal/context: {goal}' if goal else ''}
Example format: Name (2-4 words), Structure (one sentence), Visual (one sentence with specific details). e.g. "Before/After Split" / "Side-by-side comparison" / "Split screen, clear contrast".
Structure the user's idea into a proper concept format.
Return JSON:
{{
"name": "Short descriptive name (2-4 words)",
"structure": "How to structure the visual/ad (one sentence)",
"visual": "Visual guidance for the image (one sentence, specific details)"
}}"""
response_format = {
"type": "json_schema",
"json_schema": {
"name": "refined_concept",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"structure": {"type": "string"},
"visual": {"type": "string"},
},
"required": ["name", "structure", "visual"],
},
},
}
response = await llm_service.generate(
prompt=prompt,
temperature=0.7,
response_format=response_format,
)
result = json.loads(response)
result["key"] = "custom"
result["category"] = "Custom"
result["original_text"] = text
return result
# --- Matrix prompt builders ---
def _build_matrix_ad_prompt(
self,
niche: str,
angle: Dict[str, Any],
concept: Dict[str, Any],
niche_data: Dict[str, Any],
cta: str,
core_motivator: Optional[str] = None,
target_audience: Optional[str] = None,
offer: Optional[str] = None,
) -> str:
"""Build ad copy prompt using angle + concept framework."""
# AI will decide whether to include numbers based on ad format and strategy
# Always provide guidance, AI decides usage
# Get niche-specific numbers guidance from number_config type (AI decides if/when to use)
numbers = self._generate_niche_numbers(niche)
num_type = niche_data.get("number_config", {}).get("type", "savings")
if num_type == "weight_loss":
numbers_section = f"""SPECIFIC NUMBERS TO USE:
- Weight Lost: {numbers['difference']}
- Timeframe: {numbers['days']}
- Starting: {numbers['before']}, Current: {numbers['after']}"""
else:
price_guidance = self._generate_specific_price(niche)
numbers_section = f"""NUMBERS GUIDANCE (you decide if/when to use):
- Price Guidance: {price_guidance}
- Saved: {numbers['difference']}/year
- Before: {numbers['before']}, After: {numbers['after']}
DECISION: Include prices/numbers only if they enhance believability and fit the ad format/strategy.
Use oddly specific amounts (e.g., "$97.33" not "$100") when including prices."""
# Note: AI decides whether to use the numbers based on format and strategy
motivator_block = ""
if core_motivator:
motivator_block = f"""
=== CORE EMOTIONAL MOTIVATOR (USE THIS) ===
The user chose this motivator: "{core_motivator}"
Use it to guide the hook, headline, and primary text. The ad must amplify this motivator.
"""
return f"""You are an elite direct-response copywriter creating a Facebook ad.
{motivator_block}
=== ANGLE × CONCEPT FRAMEWORK ===
ANGLE: {angle.get('name')} (Trigger: {angle.get('trigger')}) — Example: "{angle.get('example')}"
CONCEPT: {concept.get('name')} — Structure: {concept.get('structure')} | Visual: {concept.get('visual')}
For variety, adapt to different ecom verticals (fashion, beauty, supplements, fitness, electronics, home, pets, food). This ad can lean into: {get_random_vertical()['name']}.
{f'=== USER INPUTS ===' if target_audience or offer else ''}
{f'TARGET AUDIENCE: {target_audience}' if target_audience else ''}
{f'OFFER: {offer}' if offer else ''}
=== CONTEXT ===
NICHE: {niche.replace("_", " ").title()} | CTA: {cta}
{numbers_section}
=== OUTPUT (JSON) ===
{{
"headline": "<10 words max; use angle/trigger above; add numbers if they strengthen>",
"primary_text": "<2-3 emotional sentences>",
"description": "<one sentence, 10 words max>",
"body_story": "<8-12 sentence story: pain, tension, transformation, hope; first/second person>",
"image_brief": "<scene following concept above>",
"cta": "{cta}",
"psychological_angle": "{angle.get('name')}",
"why_it_works": "<brief mechanism>"
}}
Generate the ad now. Be bold and specific."""
def _build_matrix_image_prompt(
self,
niche: str,
angle: Dict[str, Any],
concept: Dict[str, Any],
ad_copy: Dict[str, Any],
core_motivator: Optional[str] = None,
) -> str:
"""Build image prompt using concept's visual guidance."""
headline = ad_copy.get("headline", "")
# When user selected a motivator, show it on the image instead of headline (like title)
text_on_image = (core_motivator or "").strip() or (headline or "")
use_motivator = bool((core_motivator or "").strip())
image_brief = ad_copy.get("image_brief", "")
# Natural blend options: motivator/text feels part of the scene, not forced
natural_blend_options = [
"as a handwritten note or sticky note in the scene",
"as text on a phone screen, tablet, or laptop in frame",
"as a message in a chat or social feed within the image",
"on a sign, whiteboard, or poster that fits the environment",
"as a caption or note lying naturally in the scene",
"reflected in a mirror or window as part of the environment",
"on a document, receipt, or paper in the scene",
"as subtle text on a product or package in frame",
]
text_blend = random.choice(natural_blend_options)
# Fallback simpler style when not using motivator
text_styles = [
"naturally integrated into the scene",
"as part of a document or sign in the image",
"on a surface within the scene",
"as natural text element in the environment",
]
text_style = random.choice(text_styles)
# Get niche-specific guidance from niche data
niche_data_img = self._get_niche_data(niche)
niche_guidance = niche_data_img.get("image_niche_guidance_short", "").strip() or f"NICHE: {niche.replace('_', ' ').title()}"
if use_motivator:
text_section = f'''=== MOTIVATOR PHRASE (blend naturally into the scene) ===
Phrase: "{text_on_image}"
CRITICAL — natural blend only:
- Weave this phrase into the scene so it feels organic, not overlaid or forced.
- It could appear {text_blend}.
- The scene and concept come first; the phrase should feel like a natural part of that world.
- No banners, boxes, or decorative overlays. No "ad-like" text placement.
- Text must be READABLE and correctly spelled, but never the dominant focal point.
- Avoid centering the phrase or making it look pasted on. It should belong in the environment.'''
layout_section = f"""=== LAYOUT ===
- Prioritize the scene from the brief and the {concept.get('name')} concept.
- The phrase "{text_on_image}" may appear anywhere it fits naturally (e.g. on a device, note, sign, or surface in frame).
- Do NOT force a dedicated "text zone." Let the composition feel organic."""
closing = f'''Create a scroll-stopping, authentic image. The phrase "{text_on_image}" may appear naturally in the scene—never forced or overlay-style.'''
else:
text_section = f'''=== HEADLINE TEXT (if included, should be part of natural scene) ===
"{text_on_image}"
TEXT REQUIREMENTS (natural integration, NOT overlay):
- Text should appear as part of the scene (on documents, signs, surfaces)
- Position: {text_style}
- Must be READABLE
- Spell every word correctly
- CRITICAL: NO overlay boxes, banners, or decorative elements
- Text should look like it naturally belongs in the scene'''
layout_section = f"""=== LAYOUT ===
- Text zone (bottom 25%): "{text_on_image}"
- Visual zone (top 75%): Scene following {concept.get('name')} concept"""
closing = f'''Create a scroll-stopping ad image with "{text_on_image}" prominently displayed.'''
prompt = f"""Create a Facebook ad image with natural, authentic content.
=== CAMERA QUALITY ===
- The image should look like it was shot from a low quality camera
- Include characteristics of low quality camera: slight grain, reduced sharpness, lower resolution appearance, authentic camera imperfections
- Should have the authentic feel of a real photo taken with a basic or older camera device
{text_section}
=== VISUAL CONCEPT: {concept.get('name')} ===
Structure: {concept.get('structure')}
Visual Guidance: {concept.get('visual')}
=== SCENE FROM BRIEF ===
{image_brief}
=== PSYCHOLOGICAL ANGLE: {angle.get('name')} ===
This image should trigger: {angle.get('trigger')}
{niche_guidance}
=== PEOPLE AND FACES: OPTIONAL ===
Only include people or faces if the image brief/VISUAL SCENE explicitly describes them. Creatives can be product-only, document-only, or layout-only with no people. If this image does include people or faces, they MUST look like real, original people with:
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
- Natural facial asymmetry (no perfectly symmetrical faces)
- Unique, individual facial features (not generic or model-like)
- Natural expressions with authentic micro-expressions
- Realistic skin tones with natural variations and undertones
- Natural hair texture with individual strands
- Faces that look like real photographs of real people, NOT AI-generated portraits
- Avoid any faces that look synthetic, fake, or obviously computer-generated
{layout_section}
=== AVOID ===
- Missing or misspelled text
- Text that is too small to read
- Generic stock photo look
- Watermarks, logos
- AI-generated faces, synthetic faces, or fake-looking faces
- Overly smooth or plastic-looking skin
- Perfectly symmetrical faces
- Generic or model-like facial features
- Uncanny valley faces
- Faces that look like they came from a face generator
- Overly perfect, flawless skin
- Cartoon-like or stylized faces
{chr(10) + "- Forced or overlay-style text; the motivator must feel naturally mixed into the scene" if use_motivator else ""}
{closing}"""
# Refine and clean the prompt before sending (pass niche for demographic fixes)
refined_prompt = self._refine_image_prompt(prompt, niche=niche)
return refined_prompt
# --- Public: batch ---
async def generate_batch(
self,
niche: str,
count: int = 5,
images_per_ad: int = 1,
image_model: Optional[str] = None,
username: Optional[str] = None, # Username of the user generating the ads
method: Optional[str] = None, # "standard", "matrix", or None (mixed)
target_audience: Optional[str] = None,
offer: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
Generate multiple ad creatives - PARALLELIZED.
Uses semaphore to limit concurrent operations and prevent resource exhaustion.
Args:
niche: Target niche
count: Number of ads to generate
images_per_ad: Images per ad (typically 1 for batch)
image_model: Image model to use
username: Username of the user generating the ads
method: Generation method - "standard", "matrix", or None (mixed 50/50)
Returns:
List of ad results (all normalized to GenerateResponse format)
"""
# Use semaphore to limit concurrent ad generation (max 3 at a time to avoid overwhelming APIs)
semaphore = asyncio.Semaphore(3)
async def generate_single_ad(ad_index: int):
"""Helper function to generate a single ad with semaphore control."""
async with semaphore:
try:
# Use method parameter to determine generation type
if method == "matrix":
use_matrix = True
elif method == "standard":
use_matrix = False
else:
# Default: 50% standard, 50% matrix (ensures all resources used)
use_matrix = random.random() < 0.5
if use_matrix:
# Use angle × concept matrix approach
result = await self.generate_ad_with_matrix(
niche=niche,
num_images=images_per_ad,
image_model=image_model,
username=username, # Pass username
target_audience=target_audience,
offer=offer,
)
# Normalize matrix result to standard format for batch response
# Extract matrix info and convert metadata
matrix_info = result.get("matrix", {})
angle = matrix_info.get("angle", {})
concept = matrix_info.get("concept", {})
# Convert to standard AdMetadata format
result["metadata"] = {
"strategies_used": [angle.get("trigger", "emotional_trigger")],
"creative_direction": f"Angle: {angle.get('name', '')}, Concept: {concept.get('name', '')}",
"visual_mood": concept.get("visual", "").split(".")[0] if concept.get("visual") else "authentic",
"framework": None,
"camera_angle": None,
"lighting": None,
"composition": None,
"hooks_inspiration": [angle.get("example", "")] if angle.get("example") else [],
"visual_styles": [concept.get("structure", "")] if concept.get("structure") else [],
}
# Remove matrix field as it's not in GenerateResponse
result.pop("matrix", None)
else:
# Use standard framework-based approach
result = await self.generate_ad(
niche=niche,
num_images=images_per_ad,
image_model=image_model,
username=username, # Pass username
target_audience=target_audience,
offer=offer,
)
return result
except Exception as e:
return {
"error": str(e),
"index": ad_index,
}
# Generate all ads in parallel using asyncio.gather with semaphore control
print(f"🔄 Generating {count} ads in parallel (max 3 concurrent)...")
ad_tasks = [generate_single_ad(i) for i in range(count)]
results = await asyncio.gather(*ad_tasks)
return results
# Global instance
ad_generator = AdGenerator()
|