File size: 18,624 Bytes
0faf659
 
 
 
 
d392f23
 
 
 
 
 
 
 
 
 
0890748
 
 
 
 
0faf659
342e0fb
0faf659
342e0fb
0faf659
 
 
 
342e0fb
 
0faf659
 
342e0fb
 
0faf659
342e0fb
d392f23
0890748
d392f23
342e0fb
d392f23
0890748
0faf659
 
 
 
 
0890748
0faf659
 
 
 
 
0890748
d392f23
 
0890748
d392f23
 
0890748
d392f23
 
 
 
0faf659
d392f23
0faf659
d392f23
 
0890748
 
d392f23
 
0890748
d392f23
 
 
0890748
d392f23
342e0fb
 
d392f23
0890748
d392f23
342e0fb
0890748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0faf659
 
 
 
 
 
0890748
0faf659
 
342e0fb
 
d392f23
0890748
 
 
 
 
 
 
 
 
 
 
 
 
 
d392f23
 
 
 
0faf659
 
 
 
342e0fb
 
 
 
 
0faf659
342e0fb
 
d392f23
 
 
342e0fb
 
 
 
 
d392f23
0890748
d392f23
 
 
 
 
 
 
 
0890748
0faf659
d392f23
 
 
 
 
 
 
 
 
0890748
d392f23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0890748
342e0fb
0890748
0faf659
 
 
 
 
 
0890748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342e0fb
0890748
 
 
342e0fb
0faf659
342e0fb
0890748
 
 
 
 
 
 
 
 
 
 
342e0fb
0890748
 
 
 
 
 
 
 
 
 
 
 
 
d392f23
 
 
 
 
 
 
 
0faf659
0890748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Config — Viral Shorts Engine Configuration

Font choices based on analysis of 2M+ short-form videos (2024-2025):

SUPPORTED LANGUAGES (26 languages):
────────────────────────────────────
Arabic Script:  ar (Arabic), fa (Persian/Farsi), ur (Urdu)
CJK:            zh (Simplified), zh-tw (Traditional), ja (Japanese), ko (Korean)
Devanagari:     hi (Hindi), mr (Marathi), ne (Nepali)
Latin:          en, fr, es, de, pt, it, tr, nl, pl, id, vi, sv, ro
Cyrillic:       ru, uk (Ukrainian)
Hebrew:         he
Thai:           th

FONT DOWNLOAD FIX:
  Google Fonts returns woff2 for modern browsers — Pillow cannot load woff2.
  Solution: use an old IE User-Agent to force Google Fonts to return TTF URLs.
    Modern UA  → fonts.gstatic.com/s/cairo/xxx.woff2   ← Pillow FAILS
    Old IE UA  → fonts.gstatic.com/s/cairo/xxx.ttf     ← Pillow works ✅
"""
import os
import re
import requests
from dotenv import load_dotenv

load_dotenv()


class Config:
    BASE_DIR    = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    TEMP_DIR    = os.path.join(BASE_DIR, "temp")
    UPLOADS_DIR = os.path.join(BASE_DIR, "uploads")
    OUTPUTS_DIR = os.path.join(BASE_DIR, "outputs")
    LOGS_DIR    = os.path.join(BASE_DIR, "logs")

    # ─────────────────────────────────────────────────────────────────────────
    # Font Registry — Google Fonts CSS2 API URLs
    # ─────────────────────────────────────────────────────────────────────────
    FONTS = {

        # ── Latin / Universal ──────────────────────────────────────────────────
        "Montserrat-Bold.ttf":        "https://fonts.googleapis.com/css2?family=Montserrat:wght@700&display=swap",
        "Rubik-Bold.ttf":             "https://fonts.googleapis.com/css2?family=Rubik:wght@700&display=swap",
        "Oswald-Bold.ttf":            "https://fonts.googleapis.com/css2?family=Oswald:wght@700&display=swap",
        "Roboto-Bold.ttf":            "https://fonts.googleapis.com/css2?family=Roboto:wght@700&display=swap",

        # ── Arabic Script ──────────────────────────────────────────────────────
        "Tajawal-Bold.ttf":           "https://fonts.googleapis.com/css2?family=Tajawal:wght@700&display=swap",
        "Cairo-Bold.ttf":             "https://fonts.googleapis.com/css2?family=Cairo:wght@700&display=swap",
        "Almarai-Bold.ttf":           "https://fonts.googleapis.com/css2?family=Almarai:wght@800&display=swap",
        "NotoSansArabic-Bold.ttf":    "https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@700&display=swap",

        # ── Persian ────────────────────────────────────────────────────────────
        "Vazirmatn-Bold.ttf":         "https://fonts.googleapis.com/css2?family=Vazirmatn:wght@700&display=swap",

        # ── Urdu ───────────────────────────────────────────────────────────────
        "NotoSansArabicUrdu-Bold.ttf": "https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@700&display=swap",

        # ── Hebrew ─────────────────────────────────────────────────────────────
        "FrankRuhlLibre-Bold.ttf":    "https://fonts.googleapis.com/css2?family=Frank+Ruhl+Libre:wght@700&display=swap",
        "Heebo-Bold.ttf":             "https://fonts.googleapis.com/css2?family=Heebo:wght@700&display=swap",

        # ── CJK ───────────────────────────────────────────────────────────────
        "NotoSansSC-Bold.ttf":        "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@700&display=swap",
        "NotoSansTC-Bold.ttf":        "https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@700&display=swap",
        "NotoSansJP-Bold.ttf":        "https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@700&display=swap",
        "NotoSansKR-Bold.ttf":        "https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@700&display=swap",

        # ── Devanagari ────────────────────────────────────────────────────────
        "NotoSansDevanagari-Bold.ttf": "https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@700&display=swap",
        "Poppins-Bold.ttf":           "https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap",

        # ── Thai ──────────────────────────────────────────────────────────────
        "Sarabun-Bold.ttf":           "https://fonts.googleapis.com/css2?family=Sarabun:wght@700&display=swap",
        "NotoSansThai-Bold.ttf":      "https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@700&display=swap",

        # ── Universal fallback ─────────────────────────────────────────────────
        "NotoSans-Bold.ttf":          "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@700&display=swap",
    }

    # ─────────────────────────────────────────────────────────────────────────
    # Language → Font
    # ─────────────────────────────────────────────────────────────────────────
    LANGUAGE_FONT_MAP = {
        "ar": "Tajawal-Bold.ttf",
        "fa": "Vazirmatn-Bold.ttf",
        "ur": "NotoSansArabic-Bold.ttf",
        "he": "Heebo-Bold.ttf",
        "zh":    "NotoSansSC-Bold.ttf",
        "zh-tw": "NotoSansTC-Bold.ttf",
        "ja":    "NotoSansJP-Bold.ttf",
        "ko":    "NotoSansKR-Bold.ttf",
        "hi": "NotoSansDevanagari-Bold.ttf",
        "mr": "NotoSansDevanagari-Bold.ttf",
        "ne": "NotoSansDevanagari-Bold.ttf",
        "th": "Sarabun-Bold.ttf",
        "ru": "Montserrat-Bold.ttf",
        "uk": "Montserrat-Bold.ttf",
        "en": "Montserrat-Bold.ttf",
        "fr": "Montserrat-Bold.ttf",
        "es": "Montserrat-Bold.ttf",
        "de": "Montserrat-Bold.ttf",
        "pt": "Montserrat-Bold.ttf",
        "it": "Montserrat-Bold.ttf",
        "tr": "Montserrat-Bold.ttf",
        "nl": "Montserrat-Bold.ttf",
        "pl": "Montserrat-Bold.ttf",
        "id": "Montserrat-Bold.ttf",
        "vi": "Roboto-Bold.ttf",
        "sv": "Montserrat-Bold.ttf",
        "ro": "Montserrat-Bold.ttf",
        "default": "NotoSans-Bold.ttf",
    }

    STYLE_FONT_MAP = {
        "classic":       "Montserrat-Bold.ttf",
        "modern_glow":   "Rubik-Bold.ttf",
        "tiktok_bold":   "Montserrat-Bold.ttf",
        "tiktok_neon":   "Montserrat-Bold.ttf",
        "youtube_clean": "Rubik-Bold.ttf",
        "youtube_box":   "Montserrat-Bold.ttf",
    }

    UNICODE_SCRIPT_RANGES = [
        ("\u0600", "\u06FF", "ar"),
        ("\u0750", "\u077F", "ar"),
        ("\u08A0", "\u08FF", "ar"),
        ("\u0590", "\u05FF", "he"),
        ("\uAC00", "\uD7AF", "ko"),
        ("\u1100", "\u11FF", "ko"),
        ("\u4E00", "\u9FFF", "zh"),
        ("\u3400", "\u4DBF", "zh"),
        ("\u3040", "\u309F", "ja"),
        ("\u30A0", "\u30FF", "ja"),
        ("\u0900", "\u097F", "hi"),
        ("\u0E00", "\u0E7F", "th"),
        ("\u0400", "\u04FF", "ru"),
        ("\u0500", "\u052F", "ru"),
    ]

    RTL_LANGUAGES = {"ar", "fa", "ur", "he"}

    DEFAULT_SIZE         = (1080, 1920)
    CHUNK_SIZE_SECONDS   = 600
    OVERLAP_SECONDS      = 60

    STYLES = [
        "cinematic",
        "cinematic_blur",
        "vertical_full",
        "split_vertical",
        "split_horizontal",
    ]

    # ─────────────────────────────────────────────────────────────────────────
    # Directory setup
    # ─────────────────────────────────────────────────────────────────────────
    @classmethod
    def setup_dirs(cls):
        for d in [cls.TEMP_DIR, cls.UPLOADS_DIR, cls.OUTPUTS_DIR, cls.LOGS_DIR]:
            os.makedirs(d, exist_ok=True)

    # ─────────────────────────────────────────────────────────────────────────
    # Language detection
    # ─────────────────────────────────────────────────────────────────────────
    @classmethod
    def detect_language_from_text(cls, text: str) -> str | None:
        if not text:
            return None
        for start, end, lang in cls.UNICODE_SCRIPT_RANGES:
            if any(start <= c <= end for c in text):
                return lang
        return None

    @classmethod
    def is_rtl(cls, language: str) -> bool:
        return language in cls.RTL_LANGUAGES

    @classmethod
    def get_font_for_language(cls, language: str, style_name: str = None) -> str:
        NON_LATIN = {
            "ar", "fa", "ur", "he",
            "zh", "zh-tw", "ja", "ko",
            "hi", "mr", "ne", "th",
        }
        if language in NON_LATIN:
            return cls.LANGUAGE_FONT_MAP.get(language, cls.LANGUAGE_FONT_MAP["default"])
        if style_name and style_name in cls.STYLE_FONT_MAP:
            return cls.STYLE_FONT_MAP[style_name]
        if language in cls.LANGUAGE_FONT_MAP:
            return cls.LANGUAGE_FONT_MAP[language]
        return cls.LANGUAGE_FONT_MAP["default"]

    # ─────────────────────────────────────────────────────────────────────────
    # Font URL extraction
    # ─────────────────────────────────────────────────────────────────────────
    @staticmethod
    def get_urls(css_content: str, prefer_latin: bool = True) -> list:
        """
        Extracts font file URLs from a Google Fonts CSS response.
        Prefers TTF over woff2 because Pillow cannot load woff2.
        """
        # Extract all (subset_comment, url) pairs
        pattern = re.compile(
            r'/\*\s*\[?\d*\]?\s*([\w\-]+)\s*\*/[^}]*?url\(([^)]+)\)',
            re.DOTALL,
        )
        pairs = pattern.findall(css_content)

        if pairs:
            subset_map = {s.lower(): u.strip().strip("'\"") for s, u in pairs}
            if prefer_latin:
                for key in ("latin", "latin-ext"):
                    if key in subset_map:
                        return [subset_map[key]]
                return [list(subset_map.values())[-1]]
            else:
                return [list(subset_map.values())[0]]

        # Fallback: grab all raw URLs
        all_urls = re.findall(r'url\(([^)]+)\)', css_content)
        all_urls = [u.strip().strip("'\"") for u in all_urls]

        # Prefer TTF, then woff (not woff2 — Pillow can't open woff2)
        ttf   = [u for u in all_urls if u.endswith(".ttf")]
        woff  = [u for u in all_urls if u.endswith(".woff") and not u.endswith(".woff2")]
        return ttf or woff or all_urls

    # ─────────────────────────────────────────────────────────────────────────
    # Font CSS download  ← FIXED: uses TTF-forcing User-Agent
    # ─────────────────────────────────────────────────────────────────────────
    @staticmethod
    def download_font_from_css(css_url: str, output_path: str) -> bool:
        """
        Downloads the correct font file for a given Google Fonts CSS URL.

        KEY FIX: Uses an old IE 6 User-Agent to force Google Fonts to return
        TTF URLs instead of woff2. Pillow/FreeType cannot open woff2 files.

          Modern Chrome UA → Google returns .woff2  → Pillow FAILS ❌
          Old IE 6 UA      → Google returns .ttf    → Pillow works ✅

        Two-pass strategy:
          Pass 1: Old IE UA → gets TTF (ideal for Pillow)
          Pass 2: Modern UA → gets woff2 as last resort (may fail in Pillow)
        """
        # ── User-Agent constants ──────────────────────────────────────────────
        # IE 6 on Windows XP — forces Google Fonts to return legacy TTF format
        UA_TTF = (
            "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; "
            "SV1; .NET CLR 1.1.4322)"
        )
        # Modern Chrome — returns woff2 (not ideal for Pillow, last resort)
        UA_MODERN = (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/124.0.0.0 Safari/537.36"
        )

        NON_LATIN_KEYWORDS = (
            "arabic", "noto", "devanagari", "sc", "jp", "kr", "tc",
            "thai", "sarabun", "heebo", "frank", "vazir", "tajawal",
            "cairo", "almarai",
        )
        filename     = os.path.basename(output_path).lower()
        is_non_latin = any(kw in filename for kw in NON_LATIN_KEYWORDS)
        prefer_latin = not is_non_latin

        os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)

        for pass_num, ua in enumerate([UA_TTF, UA_MODERN], start=1):
            ua_label = "TTF-forcing (IE6)" if pass_num == 1 else "Modern (woff2 fallback)"
            try:
                # ── Fetch CSS ─────────────────────────────────────────────────
                resp = requests.get(
                    css_url,
                    headers={"User-Agent": ua},
                    timeout=15
                )
                resp.raise_for_status()

                urls = Config.get_urls(resp.text, prefer_latin=prefer_latin)
                if not urls:
                    print(f"⚠️  Pass {pass_num} ({ua_label}): no font URLs in CSS")
                    continue

                font_url = urls[0]
                ext = os.path.splitext(font_url.split("?")[0])[-1].lower()
                print(f"⬇️  Pass {pass_num} ({ua_label}): {ext}{font_url[:70]}…")

                # ── Download font file ────────────────────────────────────────
                font_resp = requests.get(
                    font_url,
                    headers={"User-Agent": UA_MODERN},
                    timeout=30
                )
                font_resp.raise_for_status()
                data = font_resp.content

                # ── Validate: check magic bytes ───────────────────────────────
                if len(data) < 10_000:
                    print(f"⚠️  File too small ({len(data)} B) — likely error page, skipping")
                    continue

                magic = data[:4]
                is_ttf_magic = magic in (
                    b"\x00\x01\x00\x00",   # TrueType
                    b"OTTO",               # OpenType CFF
                    b"true",               # TrueType variant
                    b"wOFF",               # WOFF (Pillow ≥ 9.2 can open)
                    b"wOF2",               # WOFF2 (Pillow may fail)
                )

                if not is_ttf_magic:
                    print(
                        f"⚠️  Pass {pass_num}: unexpected magic bytes {magic.hex()} "
                        f"(probably HTML error page) — skipping"
                    )
                    continue

                if magic == b"wOF2":
                    print(
                        f"⚠️  Pass {pass_num}: received WOFF2 — "
                        f"Pillow may not be able to open this. "
                        f"Consider installing: sudo apt-get install fonts-noto-core"
                    )

                with open(output_path, "wb") as f:
                    f.write(data)

                print(f"✅ Font saved ({len(data):,} B, {ext}): {output_path}")
                return True

            except requests.RequestException as e:
                print(f"❌ Pass {pass_num} network error: {e}")
            except Exception as e:
                print(f"❌ Pass {pass_num} unexpected error: {e}")

        # ── Both passes failed ────────────────────────────────────────────────
        print(
            f"❌ All download attempts failed for {os.path.basename(output_path)}.\n"
            f"   Fix on Ubuntu/Debian:\n"
            f"     sudo apt-get install -y fonts-noto-core fonts-arabeyes\n"
            f"   Or copy a TTF manually to: {output_path}"
        )
        return False