bep40 commited on
Commit
ca8c13b
·
verified ·
1 Parent(s): 0edf211

Restore: ai_fix2.py (full), app_entry.py, patch_extra.py, restore_runner.py, app_clean.py, wc2026_scraper.py from c93b544

Browse files
Files changed (1) hide show
  1. ai_fix2.py +207 -0
ai_fix2.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, re, subprocess, html as html_lib, json
2
+ from urllib.parse import quote_plus, urlparse, parse_qs, unquote
3
+ import requests
4
+ import ai_patch as prev
5
+ from ai_patch import app
6
+ from fastapi import Request
7
+ from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
8
+
9
+ base = prev.base
10
+
11
+
12
+ def clean(s):
13
+ return re.sub(r"\s+", " ", html_lib.unescape(s or "")).strip()
14
+
15
+
16
+ def _is_real_article_text(raw):
17
+ raw = clean(raw)
18
+ if len(raw) < 500:
19
+ return False
20
+ sentences = re.split(r"(?<=[\.\!\?])\s+", raw)
21
+ long_sentences = [s for s in sentences if len(s) > 45]
22
+ return len(long_sentences) >= 5
23
+
24
+
25
+ def _extract_ddg_url(href):
26
+ if not href:
27
+ return ""
28
+ if href.startswith("//"):
29
+ href = "https:" + href
30
+ if "duckduckgo.com/l/" in href:
31
+ try:
32
+ qs = parse_qs(urlparse(href).query)
33
+ if qs.get("uddg"):
34
+ return unquote(qs["uddg"][0])
35
+ except Exception:
36
+ pass
37
+ return href
38
+
39
+
40
+ def _ddg_article_urls(topic, limit=12):
41
+ urls = []
42
+ try:
43
+ q = quote_plus(topic + " tin tức bài viết phân tích")
44
+ r = requests.get("https://html.duckduckgo.com/html/?q=" + q, headers=base.HEADERS, timeout=18)
45
+ r.encoding = "utf-8"
46
+ from bs4 import BeautifulSoup
47
+ soup = BeautifulSoup(r.text, "lxml")
48
+ for a in soup.select("a.result__a"):
49
+ u = _extract_ddg_url(a.get("href", ""))
50
+ if not u.startswith("http"):
51
+ continue
52
+ if any(bad in u for bad in ["google.com", "youtube.com", "facebook.com", "x.com", "twitter.com"]):
53
+ continue
54
+ if u not in urls:
55
+ urls.append(u)
56
+ if len(urls) >= limit:
57
+ break
58
+ except Exception:
59
+ pass
60
+ return urls
61
+
62
+
63
+ def _rss_article_urls(topic, limit=10):
64
+ out = []
65
+ try:
66
+ url = "https://news.google.com/rss/search?q=" + quote_plus(topic) + "&hl=vi&gl=VN&ceid=VN:vi"
67
+ r = requests.get(url, headers=base.HEADERS, timeout=15)
68
+ r.encoding = "utf-8"
69
+ from bs4 import BeautifulSoup
70
+ soup = BeautifulSoup(r.text, "xml")
71
+ for it in soup.find_all("item")[:limit]:
72
+ title = it.find("title").get_text(" ", strip=True) if it.find("title") else ""
73
+ link = it.find("link").get_text(strip=True) if it.find("link") else ""
74
+ src = it.find("source").get_text(" ", strip=True) if it.find("source") else base._domain(link)
75
+ if title and link:
76
+ out.append({"title": title, "url": link, "via": src, "excerpt": title})
77
+ except Exception:
78
+ pass
79
+ return out
80
+
81
+
82
+ def _topic_source_articles(topic, limit=5):
83
+ """Scrape actual article bodies. Do not accept title-only sources."""
84
+ candidates = []
85
+ seen = set()
86
+
87
+ for u in _ddg_article_urls(topic, limit=14):
88
+ if u not in seen:
89
+ seen.add(u)
90
+ candidates.append({"url": u, "title": "", "via": base._domain(u)})
91
+
92
+ try:
93
+ _ctx, srcs = base.web_context(topic, limit=8)
94
+ for s in srcs or []:
95
+ u = s.get("url") or ""
96
+ if u.startswith("http") and u not in seen:
97
+ seen.add(u)
98
+ candidates.append(s)
99
+ except Exception:
100
+ pass
101
+
102
+ for s in _rss_article_urls(topic, limit=10):
103
+ u = s.get("url") or ""
104
+ if u.startswith("http") and u not in seen:
105
+ seen.add(u)
106
+ candidates.append(s)
107
+
108
+ out = []
109
+ for s in candidates[:24]:
110
+ url = s.get("url") or ""
111
+ try:
112
+ page = base.scrape_any_url(url)
113
+ raw = (page.get("summary", "") + "\n" + page.get("text", "")).strip()
114
+ if not _is_real_article_text(raw):
115
+ continue
116
+ title = page.get("title") or s.get("title") or url
117
+ via = page.get("via") or s.get("via") or base._domain(url)
118
+ out.append({
119
+ "title": title,
120
+ "url": url,
121
+ "raw": raw,
122
+ "image": page.get("image") or "",
123
+ "via": via,
124
+ "source": {"title": title, "url": url, "excerpt": raw[:700], "via": via}
125
+ })
126
+ if len(out) >= limit:
127
+ break
128
+ except Exception:
129
+ continue
130
+ return out[:limit]
131
+
132
+
133
+ def sentence_split(text):
134
+ text = re.sub(r"^[•\-\*]\s*", "", text or "", flags=re.M)
135
+ text = re.sub(r"\n+", ". ", text)
136
+ parts = []
137
+ for s in re.split(r"(?<=[\.\!\?])\s+", text):
138
+ s = clean(s)
139
+ if len(s) >= 8:
140
+ parts.append(s)
141
+ return parts
142
+
143
+
144
+ def srt_time(sec):
145
+ ms = int((sec - int(sec)) * 1000)
146
+ sec = int(sec)
147
+ return f"{sec//3600:02d}:{(sec%3600)//60:02d}:{sec%60:02d},{ms:03d}"
148
+
149
+
150
+ def parse_timecode(t):
151
+ t = t.replace(',', '.')
152
+ parts = t.split(':')
153
+ if len(parts) == 3:
154
+ return int(parts[0])*3600 + int(parts[1])*60 + float(parts[2])
155
+ if len(parts) == 2:
156
+ return int(parts[0])*60 + float(parts[1])
157
+ return float(parts[0])
158
+
159
+
160
+ def convert_vtt_to_scaled_srt(vtt_path, srt_path, speed=1.2):
161
+ try:
162
+ txt = open(vtt_path, 'r', encoding='utf-8').read().splitlines()
163
+ cues = []
164
+ i = 0
165
+ while i < len(txt):
166
+ line = txt[i].strip()
167
+ if '-->' in line:
168
+ a, b = [x.strip().split()[0] for x in line.split('-->')[:2]]
169
+ start = parse_timecode(a) / speed
170
+ end = parse_timecode(b) / speed
171
+ i += 1
172
+ texts = []
173
+ while i < len(txt) and txt[i].strip():
174
+ texts.append(txt[i].strip())
175
+ i += 1
176
+ s = clean(' '.join(texts))
177
+ if s:
178
+ cues.append((start, end, s))
179
+ i += 1
180
+ if not cues:
181
+ return False
182
+ with open(srt_path, 'w', encoding='utf-8') as f:
183
+ for idx, (st, en, s) in enumerate(cues, 1):
184
+ if en <= st:
185
+ en = st + 1.2
186
+ f.write(f"{idx}\n{srt_time(st)} --> {srt_time(en)}\n{s}\n\n")
187
+ return True
188
+ except Exception:
189
+ return False
190
+
191
+
192
+ def write_weighted_srt(script, path, total_duration):
193
+ subs = sentence_split(script)
194
+ if not subs:
195
+ subs = [clean(script)[:140] or "VNEWS"]
196
+ total_chars = max(1, sum(len(x) for x in subs))
197
+ usable = max(2.0, float(total_duration) - 1.0)
198
+ cur = 0.5
199
+ with open(path, "w", encoding="utf-8") as f:
200
+ for i, s in enumerate(subs, 1):
201
+ dur = max(1.8, min(7.0, usable * len(s) / total_chars))
202
+ start = cur
203
+ end = min(total_duration - 0.15, cur + dur)
204
+ cur = end + 0.18
205
+ f.write(f"{i}\n{srt_time(start)} --> {srt_time(end)}\n{s}\n\n")
206
+ if cur >= total_duration - 0.2:
207
+ break