File size: 6,982 Bytes
d4b7504
 
 
 
 
 
 
ebe5f6f
 
 
d4b7504
 
ebe5f6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4b7504
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
GearCut — Gradio app for HuggingFace Spaces.

Deploy on a Space (AMFORGE) so anyone can try GearCut in the browser.
The model (gc_editor) and tokenizer are pulled from the AMFORGE HF repos at
startup. ffmpeg is provided by packages.txt.

Core modules are loaded from a private HF repo at startup using HF_TOKEN secret.
Visitors can use the demo but cannot access the proprietary source code.

Local run:  python app.py
"""
import os
import sys
import json
import uuid

# ---- Bootstrap: download proprietary modules from private HF repo -----------
def _bootstrap_modules():
    """Download gearcut_* modules from private AMFORGE/gearcut-core repo."""
    token = os.environ.get("HF_TOKEN")
    if not token:
        print("[GearCut] WARNING: HF_TOKEN not set — assuming modules are local.")
        return

    modules = [
        "gearcut_compiler.py",
        "gearcut_model.py",
        "gearcut_infer.py",
        "gearcut_ground.py",
    ]

    # Check if already downloaded
    if all(os.path.exists(m) for m in modules):
        print("[GearCut] Core modules already present.")
        return

    print("[GearCut] Downloading core modules from private repo...")
    try:
        from huggingface_hub import hf_hub_download
        for fname in modules:
            if not os.path.exists(fname):
                path = hf_hub_download(
                    repo_id="AMFORGE/gearcut-core",
                    filename=fname,
                    repo_type="dataset",
                    token=token,
                    local_dir=".",
                )
                print(f"[GearCut]   ✓ {fname} downloaded")
            else:
                print(f"[GearCut]   ✓ {fname} already exists")
    except Exception as e:
        print(f"[GearCut] ERROR downloading modules: {e}")
        sys.exit(1)

_bootstrap_modules()

# ---- Now import the proprietary modules ------------------------------------
import gearcut_compiler as gc

_MODEL = None
def get_model():
    global _MODEL
    if _MODEL is None:
        from gearcut_infer import GearCutModel     # lazy: only when first used (needs torch)
        _MODEL = GearCutModel()
    return _MODEL


# ---- core orchestration (no Gradio here, so it stays unit-testable) ---------
def process(video_paths, instruction, preset_override=None, model=None):
    """Returns (output_video_path_or_None, operations_json_str, status_markdown)."""
    if not video_paths:
        return None, "", "⚠️ Upload at least one video."
    if not (instruction or "").strip():
        return None, "", "⚠️ Type an instruction, e.g. *remove the first 3 seconds and export as out.mp4*."

    specs, ctx = [], []
    for i, p in enumerate(video_paths, 1):
        dur = gc.probe_duration(p)
        if not dur:
            return None, "", f"⚠️ Couldn't read **{os.path.basename(p)}** — is it a valid video?"
        cid = f"c{i}"
        specs.append((cid, p, 0.0, dur))
        ctx.append((cid, os.path.basename(p), 0.0, dur))
    project = gc.project_from_clips(specs)

    try:
        ops, repairs, raw = (model or get_model()).operations(instruction, ctx)
    except Exception as e:
        return None, "", f"❌ Model error: {e}"
    if ops is None:
        return None, "", f"❌ Couldn't parse the model output:\n```\n{raw}\n```"
    if not ops:
        notes = "\n".join(f"- {r}" for r in repairs)
        return None, "[]", "⚠️ GearCut misread that — nothing safe to do. Try rephrasing.\n\n" + notes

    try:
        gc.apply_all(project, ops)
        if project.export is None:
            project.export = {"path": "out.mp4", "preset": "youtube_1080p"}
        if preset_override and preset_override in gc.PRESETS:
            project.export["preset"] = preset_override
        os.makedirs("gc_outputs", exist_ok=True)
        project.export["path"] = os.path.join("gc_outputs", f"{uuid.uuid4().hex}.mp4")
        gc.render(project)
    except gc.OpError as e:
        return None, json.dumps(ops, indent=2), f"❌ Could not apply the edit: {e}"
    except Exception as e:
        return None, json.dumps(ops, indent=2), f"❌ Render failed: {e}"

    status = "✅ **Done.**\n\n```\n" + gc.describe(project) + "\n```"
    if repairs:
        status += "\n\n**Safety adjustments (grounding):**\n" + "\n".join(f"- {r}" for r in repairs)
    return project.export["path"], json.dumps(ops, indent=2), status


# ---- Gradio UI --------------------------------------------------------------
def build_ui():
    import gradio as gr

    EXAMPLES = [
        "remove the first 3 seconds and export as out.mp4",
        "keep only 10 to 45 of clip 1 and export as clip.mp4",
        "cut the last 20 seconds off clip 1",
        "split clip 1 at 30 seconds",
        "export in tiktok_vertical",
    ]
    PRESET_CHOICES = ["(from instruction)"] + list(gc.PRESETS.keys())

    def _run(files, instruction, preset):
        paths = [f if isinstance(f, str) else getattr(f, "name", None) for f in (files or [])]
        paths = [p for p in paths if p]
        override = None if preset in (None, "(from instruction)") else preset
        return process(paths, instruction, override)

    theme = gr.themes.Soft(primary_hue="indigo", secondary_hue="violet")
    with gr.Blocks(title="GearCut", theme=theme) as demo:
        gr.Markdown(
            "# 🎬 GearCut\n"
            "**Edit video with plain language.** Type what you want — GearCut figures out the cuts "
            "and renders the result. Lightweight, FFmpeg-based, powered by the in-house **gc_editor** model (AMFORGE).")
        with gr.Row():
            with gr.Column(scale=1):
                videos = gr.File(label="Video(s) — order = sequence on the timeline",
                                 file_count="multiple", file_types=["video"], type="filepath")
                instruction = gr.Textbox(label="What do you want to do?",
                                         placeholder="remove the first 3 seconds and export as out.mp4",
                                         lines=2)
                preset = gr.Dropdown(PRESET_CHOICES, value="(from instruction)", label="Output format")
                run = gr.Button("✂️  Edit", variant="primary")
                gr.Examples(EXAMPLES, inputs=instruction, label="Try an instruction")
            with gr.Column(scale=1):
                result = gr.Video(label="Result")
                status = gr.Markdown()
                ops = gr.Code(label="Operations (what the model produced)", language="json")
        gr.Markdown(
            "**Supported (v1):** trim (start / end / range), split, join several clips, delete, reorder, export.  \n"
            "**Presets:** youtube_1080p · tiktok_vertical · instagram_square · web_720p · master_4k.  \n"
            "*Transitions, text overlays and effects are coming in v2.*")
        run.click(_run, [videos, instruction, preset], [result, ops, status])
    return demo


if __name__ == "__main__":
    build_ui().launch()