mrpoddaa commited on
Commit
de07519
Β·
verified Β·
1 Parent(s): fbfd560

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +255 -0
app.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # COMPATIBILITY PATCHES β€” must run before any other import
3
+ # ═══════════════════════════════════════════════════════════════════════════════
4
+
5
+ import sys, types
6
+
7
+ # 1) Python 3.13 removed audioop; pydub needs it via gradio 4.44
8
+ for _m in ("audioop", "pyaudioop"):
9
+ if _m not in sys.modules:
10
+ sys.modules[_m] = types.ModuleType(_m)
11
+
12
+ # 2) gradio_client schema patches (bool/non-dict schema guard)
13
+ import gradio_client.utils as _gcu
14
+
15
+ _orig_get_type = _gcu.get_type
16
+ _orig_inner = _gcu._json_schema_to_python_type
17
+ _orig_j2p = _gcu.json_schema_to_python_type
18
+
19
+ def _safe_get_type(schema):
20
+ if not isinstance(schema, dict): return {}
21
+ return _orig_get_type(schema)
22
+
23
+ def _safe_inner(schema, defs=None):
24
+ if not isinstance(schema, dict): return "Any"
25
+ return _orig_inner(schema, defs)
26
+
27
+ def _safe_j2p(schema):
28
+ if not isinstance(schema, dict): return "Any"
29
+ return _orig_j2p(schema)
30
+
31
+ _gcu.get_type = _safe_get_type
32
+ _gcu._json_schema_to_python_type = _safe_inner
33
+ _gcu.json_schema_to_python_type = _safe_j2p
34
+
35
+ # 3) FIX: gradio 4.44.0 calls templates.TemplateResponse(dict, dict) β€” passes
36
+ # the context dict as the first arg (template name) instead of a string.
37
+ # Starlette's TemplateResponse signature is (name: str, context: dict, ...).
38
+ # We patch TemplateResponse to detect the swapped args and correct them.
39
+ import starlette.templating as _st
40
+
41
+ _OrigJinja2Templates = _st.Jinja2Templates
42
+
43
+ class _PatchedJinja2Templates(_OrigJinja2Templates):
44
+ def TemplateResponse(self, *args, **kwargs):
45
+ # Gradio 4.44 bug: passes (dict_context, dict_context) β€” first arg is
46
+ # a dict instead of the template name string.
47
+ if args and isinstance(args[0], dict):
48
+ ctx = args[0]
49
+ # The real template name lives in the context under "request" scope;
50
+ # gradio always uses "index.html" as its main template.
51
+ name = ctx.pop("__template_name__", None) or "index.html"
52
+ # Reconstruct as (name, context, *rest)
53
+ args = (name, ctx) + args[1:]
54
+ return super().TemplateResponse(*args, **kwargs)
55
+
56
+ # Patch into starlette.templating so any already-imported reference is updated
57
+ _st.Jinja2Templates = _PatchedJinja2Templates
58
+
59
+ # Also patch it into gradio.routes which imported Jinja2Templates at load time
60
+ try:
61
+ import gradio.routes as _gr
62
+ if hasattr(_gr, 'templates') and _gr.templates is not None:
63
+ _gr.templates.__class__ = _PatchedJinja2Templates
64
+ except Exception:
65
+ pass
66
+
67
+ # ═══════════════════════════════════════════════════════════════════════════════
68
+ # MAIN APP
69
+ # ═══════════════════════════════════════════════════════════════════════════════
70
+
71
+ import gradio as gr
72
+ import re, os, tempfile
73
+ from huggingface_hub import InferenceClient
74
+
75
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
76
+ MODEL_ID = "google/gemma-2-9b-it"
77
+ client = InferenceClient(model=MODEL_ID, token=HF_TOKEN)
78
+
79
+ # ── SRT helpers ────────────────────────────────────────────────────────────────
80
+
81
+ def parse_srt(content: str) -> list:
82
+ blocks = []
83
+ for block in re.split(r'\n\s*\n', content.strip()):
84
+ lines = block.strip().splitlines()
85
+ if len(lines) < 2 or not lines[0].strip().isdigit() or '-->' not in lines[1]:
86
+ continue
87
+ blocks.append({
88
+ 'number': int(lines[0].strip()),
89
+ 'timecode': lines[1].strip(),
90
+ 'text': '\n'.join(lines[2:]).strip(),
91
+ })
92
+ return blocks
93
+
94
+ def blocks_to_srt(blocks: list) -> str:
95
+ return '\n\n'.join(
96
+ f"{i}\n{b['timecode']}\n{b['text']}"
97
+ for i, b in enumerate(blocks, 1)
98
+ ) + '\n'
99
+
100
+ # ── Detection ──────────────────────────────────────────────────────────────────
101
+
102
+ REMOVE_RE = re.compile(
103
+ r'https?://\S+|www\.\S+\.\S+'
104
+ r'|subscene|opensubtitles|addic7ed|podnapisi|subdownloader'
105
+ r'|subtitles?\s+by\s+\w+|translated\s+by\s+\w+'
106
+ r'|encoded\s+by|synced\s+by|ripped\s+by|corrected\s+by'
107
+ r'|\bcopyright\b|Β©|all\s+rights\s+reserved'
108
+ r'|downloaded\s+from|subtitle\s+group',
109
+ re.IGNORECASE
110
+ )
111
+
112
+ def is_metadata(text: str) -> bool:
113
+ return bool(REMOVE_RE.search(text))
114
+
115
+ def gemma_classify(text: str) -> str:
116
+ if not text.strip():
117
+ return 'REMOVE'
118
+ prompt = (
119
+ "<start_of_turn>user\n"
120
+ "Is this subtitle text metadata (credits/copyright/website/promo) or real dialogue?\n"
121
+ "Reply ONLY 'REMOVE' or 'KEEP'.\n\nText:\n" + text +
122
+ "\n<end_of_turn>\n<start_of_turn>model\n"
123
+ )
124
+ try:
125
+ r = client.text_generation(prompt, max_new_tokens=10, temperature=0.1, do_sample=False)
126
+ return 'REMOVE' if 'REMOVE' in r.strip().upper() else 'KEEP'
127
+ except Exception as e:
128
+ print(f"[Gemma] {e}")
129
+ return 'KEEP'
130
+
131
+ # ── Core ───────────────────────────────────────────────────────────────────────
132
+
133
+ def process(content: str, use_gemma: bool):
134
+ blocks = parse_srt(content)
135
+ if not blocks:
136
+ return "", "⚠️ ΰΆšΰ·’ΰ·ƒΰ·’ΰΆΈ subtitle block ΰΆ‘ΰΆšΰΆšΰ·Š ΰ·„ΰΆΈΰ·” ΰΆ±ΰ·œΰ·€ΰ·“ΰΆΊ."
137
+
138
+ kept, log = [], []
139
+ for b in blocks:
140
+ t = b['text']
141
+ if is_metadata(t):
142
+ log.append(f"#{b['number']} [REGEX]: {t[:80]}")
143
+ continue
144
+ if use_gemma and t.strip() and gemma_classify(t) == 'REMOVE':
145
+ log.append(f"#{b['number']} [GEMMA]: {t[:80]}")
146
+ continue
147
+ kept.append(b)
148
+
149
+ report = [
150
+ f"βœ… ΰ·ƒΰΆΈΰ·ŠΰΆ΄ΰ·–ΰΆ»ΰ·ŠΰΆ« blocks: {len(blocks)}",
151
+ f"πŸ—‘οΈ ΰΆ‰ΰ·€ΰΆ­ΰ·Š ΰΆšΰ·… blocks: {len(blocks)-len(kept)}",
152
+ f"βœ”οΈ ΰΆ‰ΰΆ­ΰ·’ΰΆ»ΰ·’ blocks: {len(kept)}", "",
153
+ ]
154
+ if log:
155
+ report += ["ΰΆ‰ΰ·€ΰΆ­ΰ·Š ΰΆšΰ·… lines:"] + [f" β€’ {l}" for l in log]
156
+
157
+ return blocks_to_srt(kept), '\n'.join(report)
158
+
159
+ def save_tmp(text: str) -> str:
160
+ f = tempfile.NamedTemporaryFile(mode='w', suffix='_cleaned.srt',
161
+ encoding='utf-8', delete=False)
162
+ f.write(text); f.close()
163
+ return f.name
164
+
165
+ # ── Handlers ───────────────────────────────────────────────────────────────────
166
+
167
+ def handle_file(path, use_gemma):
168
+ if not path:
169
+ return "", "ΰΆœΰ·œΰΆ±ΰ·”ΰ·€ΰΆšΰ·Š ࢭෝࢻࢱ්ࢱ.", None
170
+ try:
171
+ with open(path, encoding='utf-8-sig', errors='replace') as f:
172
+ content = f.read()
173
+ except Exception as e:
174
+ return "", f"❌ {e}", None
175
+ cleaned, report = process(content, use_gemma)
176
+ return cleaned, report, save_tmp(cleaned) if cleaned else None
177
+
178
+ def handle_text(text, use_gemma):
179
+ if not text.strip():
180
+ return "", "⚠️ SRT content ΰΆ‡ΰΆ­ΰ·”ΰ·…ΰ·” ࢚ࢻࢱ්ࢱ.", None
181
+ cleaned, report = process(text, use_gemma)
182
+ return cleaned, report, save_tmp(cleaned) if cleaned else None
183
+
184
+ # ── UI ─────────────────────────────────────────────────────────────────────────
185
+
186
+ css = ".mono{font-family:monospace;font-size:13px} footer{display:none!important}"
187
+
188
+ with gr.Blocks(title="🎬 Sinhala SRT Cleaner", theme=gr.themes.Soft(), css=css) as demo:
189
+
190
+ gr.Markdown("# 🎬 ΰ·ƒΰ·’ΰΆ‚ΰ·„ΰΆ½ SRT Subtitle Cleaner\n**Gemma 2 9B** ࢷාවිࢭා ࢚ࢻ author credits, website URLs, copyright notices ΰΆ‰ΰ·€ΰΆ­ΰ·Š ΰΆšΰΆ»ΰΆΊΰ·’.")
191
+
192
+ with gr.Tabs():
193
+
194
+ with gr.TabItem("πŸ“ ΰΆœΰ·œΰΆ±ΰ·” Upload"):
195
+ with gr.Row():
196
+ with gr.Column(scale=1):
197
+ file_in = gr.File(label="SRT ΰΆœΰ·œΰΆ±ΰ·”ΰ·€", file_types=[".srt"], type="filepath")
198
+ gemma_cb1 = gr.Checkbox(label="πŸ€– Gemma AI", value=True,
199
+ info="Uncheck β†’ Regex only (ΰΆ‰ΰΆšΰ·ŠΰΆΈΰΆ±ΰ·Š)")
200
+ btn1 = gr.Button("πŸš€ Process", variant="primary", size="lg")
201
+ with gr.Column(scale=2):
202
+ report1 = gr.Textbox(label="πŸ“Š Report", lines=8, interactive=False)
203
+ preview1 = gr.Textbox(label="βœ… Cleaned Preview", lines=15,
204
+ interactive=False, elem_classes="mono")
205
+ dl1 = gr.File(label="⬇️ Download")
206
+ btn1.click(fn=handle_file, inputs=[file_in, gemma_cb1],
207
+ outputs=[preview1, report1, dl1])
208
+
209
+ with gr.TabItem("πŸ“ Text Paste"):
210
+ with gr.Row():
211
+ with gr.Column():
212
+ text_in = gr.Textbox(label="SRT Content", lines=15, elem_classes="mono",
213
+ placeholder="1\n00:00:01,000 --> 00:00:04,000\nExample...")
214
+ gemma_cb2 = gr.Checkbox(label="πŸ€– Gemma AI", value=True)
215
+ btn2 = gr.Button("πŸš€ Process", variant="primary")
216
+ with gr.Column():
217
+ report2 = gr.Textbox(label="πŸ“Š Report", lines=6, interactive=False)
218
+ preview2 = gr.Textbox(label="βœ… Cleaned Output", lines=15,
219
+ interactive=False, elem_classes="mono")
220
+ dl2 = gr.File(label="⬇️ Download")
221
+ btn2.click(fn=handle_text, inputs=[text_in, gemma_cb2],
222
+ outputs=[preview2, report2, dl2])
223
+
224
+ with gr.TabItem("πŸ”Œ API"):
225
+ gr.Markdown("""
226
+ ## API Endpoint
227
+ ### Python β€” File
228
+ ```python
229
+ from gradio_client import Client
230
+ client = Client("your-username/srt-cleaner")
231
+ cleaned, report, file = client.predict(
232
+ "path/to/file.srt", True, api_name="/handle_file"
233
+ )
234
+ ```
235
+ ### Python β€” Text
236
+ ```python
237
+ cleaned, report, file = client.predict(
238
+ "1\\n00:00:01,000 --> 00:00:04,000\\nwww.subscene.com\\n\\n2\\n...",
239
+ True, api_name="/handle_text"
240
+ )
241
+ ```
242
+ ### cURL
243
+ ```bash
244
+ curl -X POST "https://your-space.hf.space/run/predict" \\
245
+ -H "Content-Type: application/json" \\
246
+ -d '{"fn_index":1,"data":["SRT_CONTENT",true]}'
247
+ ```
248
+ """)
249
+
250
+ gr.Markdown("---\n**Setup:** Space Settings β†’ Secrets β†’ `HF_TOKEN` set ࢚ࢻࢱ්ࢱ.")
251
+
252
+ demo.queue(max_size=10)
253
+
254
+ if __name__ == "__main__":
255
+ demo.launch(server_name="0.0.0.0", server_port=7860)