File size: 11,857 Bytes
eda316b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from pathlib import Path

from humeo_core.primitives import compile as compile_mod
from humeo_core.primitives.compile import (
    _ensure_windows_fontconfig,
    build_ffmpeg_cmd,
    plan_title_drawtext,
)
from humeo_core.schemas import Clip, LayoutInstruction, LayoutKind, RenderRequest, RenderTheme


def _req(**overrides):
    c = Clip(clip_id="1", topic="t", start_time_sec=10.0, end_time_sec=40.0)
    li = LayoutInstruction(clip_id="1", layout=LayoutKind.SIT_CENTER)
    data = dict(
        source_path="/tmp/src.mp4",
        clip=c,
        layout=li,
        output_path="/tmp/out.mp4",
        render_theme=RenderTheme.LEGACY,
        mode="dry_run",
    )
    data.update(overrides)
    return RenderRequest(**data)


def test_ffmpeg_cmd_has_ss_duration_filtergraph_output():
    cmd = build_ffmpeg_cmd(_req())
    assert "-ss" in cmd
    assert "-t" in cmd
    assert "-filter_complex" in cmd
    # duration = 30.0
    t_idx = cmd.index("-t")
    assert float(cmd[t_idx + 1]) == 30.0
    ss_idx = cmd.index("-ss")
    assert float(cmd[ss_idx + 1]) == 10.0
    assert cmd[-1] == "/tmp/out.mp4"


def test_title_text_injects_drawtext():
    cmd = build_ffmpeg_cmd(_req(title_text="Hello: world's"))
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "drawtext" in fg
    # colon should be escaped
    assert "Hello\\:" in fg
    assert "worlds" in fg
    assert "world's" not in fg
    assert "expansion=none" in fg


def test_map_vout_and_primary_audio():
    cmd = build_ffmpeg_cmd(_req())
    assert "[vout]" in cmd
    assert "0:a:0" in cmd


def test_subtitle_style_uses_requested_font_and_margin():
    cmd = build_ffmpeg_cmd(
        _req(subtitle_path="/tmp/clip.srt", subtitle_font_size=18, subtitle_margin_v=64)
    )
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "subtitles='" in fg
    assert "FontSize=18" in fg
    assert "MarginV=64" in fg
    # Smart word wrap so long captions break into multiple readable lines.
    assert "WrapStyle=0" in fg


def test_subtitle_original_size_pins_libass_to_output_resolution():
    """Without original_size=W x H, libass uses PlayResY=288 and blows up fonts/margins.



    This is the root cause of the "subtitles floating in the middle of the

    frame / blocked" bug the user reported.

    """
    cmd = build_ffmpeg_cmd(_req(subtitle_path="/tmp/clip.srt"))
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "original_size=1080x1920" in fg


def test_subtitles_applied_after_crop_and_title():
    """Order: crop/compose -> drawtext title -> subtitles.



    The pipeline must crop **first**, then draw text on the finished frame.

    """
    cmd = build_ffmpeg_cmd(
        _req(title_text="Hook", subtitle_path="/tmp/clip.srt")
    )
    fg = cmd[cmd.index("-filter_complex") + 1]
    crop_pos = fg.index("[0:v]crop=")
    drawtext_pos = fg.index("drawtext")
    subs_pos = fg.index("subtitles=")
    assert crop_pos < drawtext_pos < subs_pos


def test_build_is_layout_specific():
    c = Clip(clip_id="1", topic="t", start_time_sec=0, end_time_sec=10)
    split_req = _req(
        clip=c,
        layout=LayoutInstruction(clip_id="1", layout=LayoutKind.SPLIT_CHART_PERSON),
    )
    cmd = build_ffmpeg_cmd(split_req)
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "vstack" in fg


def test_title_is_suppressed_on_split_layouts():
    """Split layouts already contain a slide/chart with its own title.



    Overlaying an additional drawtext title just obscures content -- that's

    what was happening in the Cathy Wood "chart overlaps subject" report.

    """
    for kind in (
        LayoutKind.SPLIT_CHART_PERSON,
        LayoutKind.SPLIT_TWO_PERSONS,
        LayoutKind.SPLIT_TWO_CHARTS,
    ):
        cmd = build_ffmpeg_cmd(
            _req(
                layout=LayoutInstruction(clip_id="1", layout=kind),
                title_text="This should not render",
            )
        )
        fg = cmd[cmd.index("-filter_complex") + 1]
        assert "drawtext" not in fg, f"title leaked into split layout {kind}"


def test_title_is_drawn_on_single_subject_layouts():
    """Titles are still rendered on ZOOM_CALL_CENTER and SIT_CENTER."""
    for kind in (LayoutKind.ZOOM_CALL_CENTER, LayoutKind.SIT_CENTER):
        cmd = build_ffmpeg_cmd(
            _req(
                layout=LayoutInstruction(clip_id="1", layout=kind),
                title_text="Hook title",
            )
        )
        fg = cmd[cmd.index("-filter_complex") + 1]
        assert "drawtext=text='Hook title'" in fg


# ---------------------------------------------------------------------------
# Title wrapping / auto-shrink (P2: fixes the "Prediction Markets vs
# Derivatives" clipped-title bug reported against the Cathy Wood run).
# ---------------------------------------------------------------------------


def test_plan_title_short_stays_single_line_at_72px():
    """Backward compat: short titles keep the pre-P2 single-drawtext form.



    Byte-identical output for short titles is important because it keeps

    previously-calibrated visual output unchanged and avoids needless cache

    churn on existing renders.

    """
    frag = plan_title_drawtext("Hook title", out_w=1080)
    assert frag is not None
    assert frag.count("drawtext=") == 1
    assert "fontsize=72" in frag
    assert "y=80" in frag
    assert "drawtext=text='Hook title'" in frag


def test_plan_title_long_wraps_to_two_lines_below_72px():
    """Long titles wrap at the best word boundary and shrink to fit.



    "Prediction Markets vs Derivatives" is 33 chars — it overflows a 1080px

    canvas at 72px. It must wrap into "Prediction Markets" / "vs Derivatives"

    (balanced halves) at a smaller font.

    """
    frag = plan_title_drawtext("Prediction Markets vs Derivatives", out_w=1080)
    assert frag is not None
    assert frag.count("drawtext=") == 2, "long titles must split into two drawtext calls"
    assert "drawtext=text='Prediction Markets'" in frag
    assert "drawtext=text='vs Derivatives'" in frag
    assert "fontsize=72" not in frag, "two-line layout must use a smaller font"
    # Both lines share the same shrunken fontsize.
    import re

    sizes = re.findall(r"fontsize=(\d+)", frag)
    assert len(sizes) == 2 and sizes[0] == sizes[1]
    assert 44 <= int(sizes[0]) <= 64


def test_plan_title_empty_returns_none():
    assert plan_title_drawtext("", out_w=1080) is None
    assert plan_title_drawtext("   ", out_w=1080) is None


def test_plan_title_single_huge_word_shrinks_instead_of_wrapping():
    """A single word cannot be word-wrapped; it must shrink to fit."""
    frag = plan_title_drawtext("Supercalifragilisticexpialidocious", out_w=1080)
    assert frag is not None
    assert frag.count("drawtext=") == 1  # no wrap possible
    assert "fontsize=72" not in frag


def test_title_uses_arial_font_not_default_serif():
    """Titles must render in Arial (matching the ASS subtitle font), not the

    platform default which is Times New Roman on Windows.



    Regression test for the "ugly serif title on the finance short" bug.

    Both the single-line and the two-line drawtext variants must carry a

    ``font=Arial`` directive so fontconfig resolves to the same family as

    the subtitle ``Fontname=Arial``.

    """
    short = plan_title_drawtext("Hook title", out_w=1080)
    assert short is not None
    assert "font=Arial" in short or "fontfile='" in short

    long_frag = plan_title_drawtext("Prediction Markets vs Derivatives", out_w=1080)
    assert long_frag is not None
    if "font=Arial" in long_frag:
        assert long_frag.count("font=Arial") == 2
    else:
        assert long_frag.count("fontfile='") == 2


def test_title_font_matches_subtitle_font_family():
    """Title overlay and subtitle captions must read as one typographic

    family. Both routes through ``build_ffmpeg_cmd`` should carry the same

    Arial reference.

    """
    cmd = build_ffmpeg_cmd(
        _req(
            title_text="Hook title",
            subtitle_path="/tmp/clip.ass",
        )
    )
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "font=Arial" in fg or "fontfile='" in fg
    assert "Fontname=Arial" in fg


def test_long_title_pipes_through_build_ffmpeg_cmd():
    """End-to-end: a long title routed through the full command builder

    produces a valid filtergraph with two drawtext filters and no syntax

    errors ffmpeg would choke on.

    """
    cmd = build_ffmpeg_cmd(_req(title_text="Prediction Markets vs Derivatives"))
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert fg.count("drawtext=") == 2
    assert "[v_prepad]drawtext=text='Prediction Markets'" in fg
    assert "[vout]" in fg
    assert ";;" not in fg  # no empty chain links
    assert ",," not in fg  # no stray commas


def test_reference_theme_draws_title_and_caption_bars():
    cmd = build_ffmpeg_cmd(
        _req(
            title_text="A Multi-Trillion Dollar Opportunity",
            subtitle_path="/tmp/clip.ass",
            render_theme=RenderTheme.REFERENCE_LOWER_THIRD,
        )
    )
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "drawbox=x=28:y=32" in fg
    assert "drawbox=x=0:y=" in fg
    assert "Fontname=Source Sans 3" in fg
    assert "Alignment=2" in fg
    assert "Outline=2" in fg


def test_reference_theme_wraps_long_titles_inside_the_title_bar():
    cmd = build_ffmpeg_cmd(
        _req(
            title_text="12% Youth Unemployment? Start a Business With AI",
            render_theme=RenderTheme.REFERENCE_LOWER_THIRD,
        )
    )
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert fg.count("drawtext=") >= 2
    assert "..." not in fg


def test_reference_theme_draws_frosted_caption_ribbon_when_subtitles_exist():
    cmd = build_ffmpeg_cmd(
        _req(
            title_text="Hook title",
            subtitle_path="/tmp/clip.ass",
            render_theme=RenderTheme.REFERENCE_LOWER_THIRD,
        )
    )
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "drawbox=x=0:y=" in fg


def test_reference_theme_allows_titles_on_split_layouts():
    cmd = build_ffmpeg_cmd(
        _req(
            layout=LayoutInstruction(clip_id="1", layout=LayoutKind.SPLIT_CHART_PERSON),
            title_text="Hook title",
            render_theme=RenderTheme.REFERENCE_LOWER_THIRD,
        )
    )
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "drawtext=" in fg


def test_native_highlight_theme_skips_title_card_and_keeps_ass_styles():
    cmd = build_ffmpeg_cmd(
        _req(
            title_text="This title should not render",
            subtitle_path="/tmp/clip.ass",
            render_theme=RenderTheme.NATIVE_HIGHLIGHT,
        )
    )
    fg = cmd[cmd.index("-filter_complex") + 1]
    assert "drawtext" not in fg
    assert "subtitles='" in fg
    assert "force_style='" not in fg


def test_ensure_windows_fontconfig_is_noop_off_windows():
    env = _ensure_windows_fontconfig()
    assert isinstance(env, dict)


def test_ensure_windows_fontconfig_creates_config(monkeypatch, tmp_path):
    monkeypatch.setattr(compile_mod.os, "name", "nt", raising=False)
    monkeypatch.delenv("FONTCONFIG_FILE", raising=False)
    monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "localappdata"))
    monkeypatch.setenv("WINDIR", str(tmp_path / "winroot"))

    env = _ensure_windows_fontconfig()

    cfg_file = Path(env["FONTCONFIG_FILE"])
    assert cfg_file.is_file()
    text = cfg_file.read_text(encoding="utf-8")
    assert (tmp_path / "winroot" / "Fonts").as_posix() in text
    assert "fontconfig-cache" in text