bep40 commited on
Commit
dcee409
·
verified ·
1 Parent(s): 26c7dba

Upload ai_patch.py

Browse files
Files changed (1) hide show
  1. ai_patch.py +95 -146
ai_patch.py CHANGED
@@ -286,7 +286,6 @@ _PATCHED_PATHS = {
286
  ('/api/url_wall', 'POST'),
287
  ('/api/rewrite_share', 'POST'),
288
  ('/api/ai/short/{post_id}', 'POST'),
289
- ('/api/auto_poster/run', 'POST'),
290
  }
291
  app.router.routes = [
292
  r for r in app.router.routes
@@ -328,7 +327,7 @@ Nội dung bài:
328
  if 'Nguồn tham khảo:' not in text:
329
  text += "\n\n" + _source_line(src)
330
  post = base.make_post(art['title'], text, art.get('image') or base.pollinations_image_url(art['title']), art.get('url') or '', 'topic_article', sources=src)
331
-
332
  # Generate slides for this post so they persist after page reload
333
  try:
334
  page_data = _scrape_article_images(art.get('url', ''))
@@ -345,7 +344,7 @@ Nội dung bài:
345
  post['slides'] = slides
346
  except Exception:
347
  pass
348
-
349
  new_posts.append(post)
350
  posts = new_posts + posts
351
  base._save_ai_wall(posts)
@@ -372,7 +371,7 @@ async def compat_url_wall(request: Request):
372
  if 'Nguồn tham khảo:' not in text:
373
  text += "\n\n" + _source_line(src)
374
  post = base.make_post(data.get('title') or 'Bài viết', text, data.get('image') or '', url, 'url', sources=src)
375
-
376
  # Generate slides so they persist after page reload
377
  slides = []
378
  try:
@@ -389,7 +388,7 @@ async def compat_url_wall(request: Request):
389
  except Exception:
390
  pass
391
  post['slides'] = slides
392
-
393
  posts = base._load_ai_wall(); posts.insert(0, post); base._save_ai_wall(posts)
394
  return JSONResponse({'post': post, 'slides': slides})
395
 
@@ -437,7 +436,7 @@ def _extract_key_points_for_slides(paragraphs, max_points=12):
437
  # Split paragraph into sentences using Vietnamese + English punctuation - GET ALL SENTENCES
438
  sentences = re.split(r'(?<=[.!?])\s+(?=[A-ZÀ-Ỹ0-9])', p)
439
  sentences = [s.strip() for s in sentences if s.strip()]
440
-
441
  for sentence in sentences:
442
  if len(points) >= max_points:
443
  break
@@ -547,7 +546,7 @@ async def compat_rewrite_share(request: Request):
547
 
548
  def _emotion_script(text, emotion):
549
  """Prepend emotion-appropriate prefix to text based on emotion type.
550
-
551
  NOTE: Prefix is NOT added to avoid cluttering Short AI speech.
552
  The emotion is still used for voice selection but content is read cleanly.
553
  """
@@ -713,7 +712,7 @@ def _make_scene_frame(post, segment, idx, total, img_path, out_path, emotion='ne
713
  if ratio>target:
714
  nh=H; nw=int(nh*ratio)
715
  else:
716
- nw=W; nh=int(nw*ratio)
717
  cover=im.resize((nw,nh)); left=(nw-W)//2; top=(nh-H)//2
718
  cover=cover.crop((left,top,left+W,top+H))
719
  bg.paste(cover,(0,0))
@@ -767,77 +766,21 @@ def _estimate_audio_duration(path, fallback=15.0):
767
  return fallback
768
 
769
 
770
- _EDGE_VOICE_MAP = {
771
- # Vietnamese
772
- 'vi-vn-hoaimyneural': 'vi-VN-HoaiMyNeural',
773
- 'vi-vn-namminhneural': 'vi-VN-NamMinhNeural',
774
- 'hoaimy': 'vi-VN-HoaiMyNeural',
775
- 'namminh': 'vi-VN-NamMinhNeural',
776
- 'nam': 'vi-VN-NamMinhNeural',
777
- 'male': 'vi-VN-NamMinhNeural',
778
- 'nu': 'vi-VN-HoaiMyNeural',
779
- 'female': 'vi-VN-HoaiMyNeural',
780
- 'mien-nam': 'vi-VN-HoaiMyNeural',
781
- # English - Multilingual
782
- 'en-us-andrewmultilingualneural': 'en-US-AndrewMultilingualNeural',
783
- 'en-au-williammultilingualneural': 'en-AU-WilliamMultilingualNeural',
784
- 'andrew': 'en-US-AndrewMultilingualNeural',
785
- 'en_andrew': 'en-US-AndrewMultilingualNeural',
786
- 'jenny': 'en-US-AndrewMultilingualNeural',
787
- 'en_jenny': 'en-US-AndrewMultilingualNeural',
788
- # Portuguese - Multilingual (ONLY Thalita)
789
- 'pt-br-thalitamultilingualneural': 'pt-BR-ThalitaMultilingualNeural',
790
- 'thalita': 'pt-BR-ThalitaMultilingualNeural',
791
- 'pt_thalita': 'pt-BR-ThalitaMultilingualNeural',
792
- 'pt_br_thalita': 'pt-BR-ThalitaMultilingualNeural',
793
- 'pt': 'pt-BR-ThalitaMultilingualNeural',
794
- 'pt_francisco': 'pt-BR-ThalitaMultilingualNeural',
795
- # French - Multilingual
796
- 'fr-fr-viviennemultilingualneural': 'fr-FR-VivienneMultilingualNeural',
797
- 'fr-fr-remymultilingualneural': 'fr-FR-RemyMultilingualNeural',
798
- 'denise': 'fr-FR-VivienneMultilingualNeural',
799
- 'fr': 'fr-FR-VivienneMultilingualNeural',
800
- 'fr_denise': 'fr-FR-VivienneMultilingualNeural',
801
- # German - Multilingual
802
- 'de-de-seraphinamultilingualneural': 'de-DE-SeraphinaMultilingualNeural',
803
- 'de-de-florianmultilingualneural': 'de-DE-FlorianMultilingualNeural',
804
- 'katja': 'de-DE-SeraphinaMultilingualNeural',
805
- 'de': 'de-DE-SeraphinaMultilingualNeural',
806
- 'de_katja': 'de-DE-SeraphinaMultilingualNeural',
807
- # Korean - Multilingual (Hyunsu, NOT SunHee)
808
- 'ko-kr-hyusumultilingualneural': 'ko-KR-HyunsuMultilingualNeural',
809
- 'ko-kr-hyunsuneural': 'ko-KR-HyunsuMultilingualNeural',
810
- 'sunhee': 'ko-KR-HyunsuMultilingualNeural',
811
- 'ko': 'ko-KR-HyunsuMultilingualNeural',
812
- 'ko_sunhee': 'ko-KR-HyunsuMultilingualNeural',
813
- # Italian - Multilingual
814
- 'it-it-giuseppemultilingualneural': 'it-IT-GiuseppeMultilingualNeural',
815
- # Spanish (keep for backward compat)
816
- 'ela': 'en-US-AndrewMultilingualNeural',
817
- 'es_ela': 'en-US-AndrewMultilingualNeural',
818
- 'es': 'en-US-AndrewMultilingualNeural',
819
- 'es_carlos': 'en-US-AndrewMultilingualNeural',
820
- # Japanese (keep for backward compat)
821
- 'nanami': 'en-US-AndrewMultilingualNeural',
822
- 'ja': 'en-US-AndrewMultilingualNeural',
823
- 'ja_nanami': 'en-US-AndrewMultilingualNeural',
824
- # Chinese (keep for backward compat)
825
- 'xiaochen': 'en-US-AndrewMultilingualNeural',
826
- 'zh': 'en-US-AndrewMultilingualNeural',
827
- 'zh_xiaochen': 'en-US-AndrewMultilingualNeural',
828
- }
829
-
830
-
831
- def _generate_short_for_post(post_id: str, voice: str = 'nu', emotion: str = 'neutral', speed: float = 1.0):
832
- """Hàm sinh short dùng chung (route /api/ai/short và auto_poster đều gọi). Trả về dict kết quả."""
833
- voice = str(voice).strip().lower()
834
- emotion = str(emotion).strip().lower()
835
- speed = max(0.85, min(1.35, float(speed or 1.0)))
836
 
837
  posts = base._load_ai_wall()
838
  post = next((p for p in posts if str(p.get('id')) == str(post_id)), None)
839
  if not post:
840
- return {'error': 'post not found'}
841
 
842
  segments = _summary_segments_from_post(post, max_segments=25)
843
  seg_hash = hashlib.md5(('|'.join(segments)+voice+emotion+str(speed)).encode('utf-8')).hexdigest()[:8]
@@ -852,41 +795,99 @@ def _generate_short_for_post(post_id: str, voice: str = 'nu', emotion: str = 'ne
852
  post['short_segments'] = segments
853
  post['short_subtitles'] = False
854
  base._save_ai_wall(posts)
855
- return {'video': post['video'], 'voice': voice, 'emotion': emotion, 'speed': speed, 'subtitles': False, 'segments': segments, 'cached': True}
856
  if base.gTTS is None:
857
- return {'error': 'gTTS chưa sẵn sàng'}
858
 
859
- edge_voice = _EDGE_VOICE_MAP.get(voice, 'vi-VN-HoaiMyNeural')
860
  work = os.path.join(base.SHORTS_DIR, base._safe_name(post_id + suffix))
861
  os.makedirs(work, exist_ok=True)
862
  img = os.path.join(work, 'image.jpg')
863
  try:
864
  base._download_image(post.get('img'), post.get('title', 'AI news'), img)
865
- part_files = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
866
  for idx, seg in enumerate(segments):
867
- frame = os.path.join(work, f'frame_{idx:02d}.jpg')
868
- aud = os.path.join(work, f'voice_{idx:02d}.mp3')
869
- aud_fast = os.path.join(work, f'voice_{idx:02d}_fast.mp3')
870
- part = os.path.join(work, f'part_{idx:02d}.mp4')
871
  _make_scene_frame(post, seg, idx, len(segments), img, frame, emotion=emotion)
872
- spoken = _emotion_script(seg, emotion)
873
  try:
874
- subprocess.run(['python', '-m', 'edge_tts', '--voice', edge_voice, '--text', spoken, '--write-media', aud], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
875
  except Exception:
876
- tld = 'com.vn' if voice in ('nu', 'female', 'mien-nam', 'hoaimy') else 'com'
877
  try:
878
  base.gTTS(spoken, lang='vi', tld=tld, slow=False).save(aud)
879
  except TypeError:
880
  base.gTTS(spoken, lang='vi', slow=False).save(aud)
881
- subprocess.run(['ffmpeg', '-y', '-i', aud, '-filter:a', f'atempo={speed}', '-vn', aud_fast], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=90)
882
- dur = _estimate_audio_duration(aud_fast, fallback=15.0) + 0.35
883
- subprocess.run(['ffmpeg', '-y', '-loop', '1', '-t', str(dur), '-i', frame, '-i', aud_fast, '-shortest', '-c:v', 'libx264', '-tune', 'stillimage', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '128k', part], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=150)
884
  part_files.append(part)
885
- concat = os.path.join(work, 'concat.txt')
886
- with open(concat, 'w', encoding='utf-8') as f:
887
  for p in part_files:
888
  f.write("file '" + p.replace("'", "'\\''") + "'\n")
889
- subprocess.run(['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', concat, '-c', 'copy', out_mp4], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=180)
890
  post['video'] = '/api/ai/short-file/' + post_id + suffix
891
  post['short_voice'] = voice
892
  post['short_emotion'] = emotion
@@ -894,25 +895,9 @@ def _generate_short_for_post(post_id: str, voice: str = 'nu', emotion: str = 'ne
894
  post['short_segments'] = segments
895
  post['short_subtitles'] = False
896
  base._save_ai_wall(posts)
897
- return {'video': post['video'], 'voice': voice, 'emotion': emotion, 'speed': speed, 'subtitles': False, 'segments': segments, 'cached': False}
898
  except Exception as e:
899
- return {'error': 'Không tạo được shorts: ' + str(e)[:220]}
900
-
901
-
902
- @app.post('/api/ai/short/{post_id}')
903
- async def patched_ai_short(post_id: str, request: Request):
904
- try:
905
- body = await request.json()
906
- except Exception:
907
- body = {}
908
- voice = str(body.get('voice', 'nu')).strip().lower()
909
- emotion = str(body.get('emotion', 'neutral')).strip().lower()
910
- speed = float(body.get('speed', 1.0) or 1.0)
911
- result = _generate_short_for_post(post_id, voice=voice, emotion=emotion, speed=speed)
912
- if 'error' in result:
913
- code = 404 if result['error'] == 'post not found' else (503 if 'gTTS' in result['error'] else 500)
914
- return JSONResponse({'error': result['error']}, status_code=code)
915
- return JSONResponse(result)
916
 
917
 
918
  @app.get('/api/ai/short-file/{file_id}')
@@ -929,40 +914,4 @@ def api_ai_shorts():
929
  return JSONResponse({'posts': posts[:80]})
930
 
931
 
932
- # ============================================================
933
- # AUTO POSTER — đăng tự động rewrite + short lúc 7h, 13h, 19h
934
- # ============================================================
935
- try:
936
- import auto_poster
937
- _AUTO_POSTER_OK = True
938
- except Exception as _ap_err:
939
- _AUTO_POSTER_OK = False
940
- print("[auto_poster] import failed:", _ap_err)
941
-
942
-
943
- @app.post('/api/auto_poster/run')
944
- async def api_auto_poster_run(request: Request):
945
- if not _AUTO_POSTER_OK:
946
- return JSONResponse({'error': 'auto_poster chưa sẵn sàng'}, status_code=503)
947
- try:
948
- body = await request.json()
949
- except Exception:
950
- body = {}
951
- topic = (body or {}).get('topic')
952
- with_short = bool((body or {}).get('with_short', True))
953
- result = auto_poster.run_once(topic=topic, with_short=with_short)
954
- return JSONResponse(result)
955
-
956
-
957
- @app.get('/api/auto_poster/status')
958
- def api_auto_poster_status():
959
- if not _AUTO_POSTER_OK:
960
- return JSONResponse({'running': False, 'error': 'auto_poster import failed'}, status_code=503)
961
- return JSONResponse(auto_poster.status())
962
-
963
-
964
- if _AUTO_POSTER_OK:
965
- auto_poster.start()
966
-
967
-
968
  app.router.routes = [r for r in app.router.routes if not (getattr(r, 'path', None) == '/' and 'GET' in getattr(r, 'methods', set()))]
 
286
  ('/api/url_wall', 'POST'),
287
  ('/api/rewrite_share', 'POST'),
288
  ('/api/ai/short/{post_id}', 'POST'),
 
289
  }
290
  app.router.routes = [
291
  r for r in app.router.routes
 
327
  if 'Nguồn tham khảo:' not in text:
328
  text += "\n\n" + _source_line(src)
329
  post = base.make_post(art['title'], text, art.get('image') or base.pollinations_image_url(art['title']), art.get('url') or '', 'topic_article', sources=src)
330
+
331
  # Generate slides for this post so they persist after page reload
332
  try:
333
  page_data = _scrape_article_images(art.get('url', ''))
 
344
  post['slides'] = slides
345
  except Exception:
346
  pass
347
+
348
  new_posts.append(post)
349
  posts = new_posts + posts
350
  base._save_ai_wall(posts)
 
371
  if 'Nguồn tham khảo:' not in text:
372
  text += "\n\n" + _source_line(src)
373
  post = base.make_post(data.get('title') or 'Bài viết', text, data.get('image') or '', url, 'url', sources=src)
374
+
375
  # Generate slides so they persist after page reload
376
  slides = []
377
  try:
 
388
  except Exception:
389
  pass
390
  post['slides'] = slides
391
+
392
  posts = base._load_ai_wall(); posts.insert(0, post); base._save_ai_wall(posts)
393
  return JSONResponse({'post': post, 'slides': slides})
394
 
 
436
  # Split paragraph into sentences using Vietnamese + English punctuation - GET ALL SENTENCES
437
  sentences = re.split(r'(?<=[.!?])\s+(?=[A-ZÀ-Ỹ0-9])', p)
438
  sentences = [s.strip() for s in sentences if s.strip()]
439
+
440
  for sentence in sentences:
441
  if len(points) >= max_points:
442
  break
 
546
 
547
  def _emotion_script(text, emotion):
548
  """Prepend emotion-appropriate prefix to text based on emotion type.
549
+
550
  NOTE: Prefix is NOT added to avoid cluttering Short AI speech.
551
  The emotion is still used for voice selection but content is read cleanly.
552
  """
 
712
  if ratio>target:
713
  nh=H; nw=int(nh*ratio)
714
  else:
715
+ nw=W; nh=int(nw/ratio)
716
  cover=im.resize((nw,nh)); left=(nw-W)//2; top=(nh-H)//2
717
  cover=cover.crop((left,top,left+W,top+H))
718
  bg.paste(cover,(0,0))
 
766
  return fallback
767
 
768
 
769
+ @app.post('/api/ai/short/{post_id}')
770
+ async def patched_ai_short(post_id: str, request: Request):
771
+ try:
772
+ body = await request.json()
773
+ except Exception:
774
+ body = {}
775
+ voice = str(body.get('voice', 'nu')).strip().lower()
776
+ emotion = str(body.get('emotion', 'neutral')).strip().lower()
777
+ speed = float(body.get('speed', 1.0) or 1.0)
778
+ speed = max(0.85, min(1.35, speed))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
 
780
  posts = base._load_ai_wall()
781
  post = next((p for p in posts if str(p.get('id')) == str(post_id)), None)
782
  if not post:
783
+ return JSONResponse({'error': 'post not found'}, status_code=404)
784
 
785
  segments = _summary_segments_from_post(post, max_segments=25)
786
  seg_hash = hashlib.md5(('|'.join(segments)+voice+emotion+str(speed)).encode('utf-8')).hexdigest()[:8]
 
795
  post['short_segments'] = segments
796
  post['short_subtitles'] = False
797
  base._save_ai_wall(posts)
798
+ return JSONResponse({'video': post['video'], 'voice': voice, 'emotion': emotion, 'speed': speed, 'subtitles': False, 'segments': segments})
799
  if base.gTTS is None:
800
+ return JSONResponse({'error': 'gTTS chưa sẵn sàng'}, status_code=503)
801
 
 
802
  work = os.path.join(base.SHORTS_DIR, base._safe_name(post_id + suffix))
803
  os.makedirs(work, exist_ok=True)
804
  img = os.path.join(work, 'image.jpg')
805
  try:
806
  base._download_image(post.get('img'), post.get('title', 'AI news'), img)
807
+ edge_voice = {
808
+ # Vietnamese
809
+ 'vi-vn-hoaimyneural': 'vi-VN-HoaiMyNeural',
810
+ 'vi-vn-namminhneural': 'vi-VN-NamMinhNeural',
811
+ 'hoaimy': 'vi-VN-HoaiMyNeural',
812
+ 'namminh': 'vi-VN-NamMinhNeural',
813
+ 'nam': 'vi-VN-NamMinhNeural',
814
+ 'male': 'vi-VN-NamMinhNeural',
815
+ 'nu': 'vi-VN-HoaiMyNeural',
816
+ 'female': 'vi-VN-HoaiMyNeural',
817
+ 'mien-nam': 'vi-VN-HoaiMyNeural',
818
+ # English - Multilingual
819
+ 'en-us-andrewmultilingualneural': 'en-US-AndrewMultilingualNeural',
820
+ 'en-au-williammultilingualneural': 'en-AU-WilliamMultilingualNeural',
821
+ 'andrew': 'en-US-AndrewMultilingualNeural',
822
+ 'en_andrew': 'en-US-AndrewMultilingualNeural',
823
+ 'jenny': 'en-US-AndrewMultilingualNeural',
824
+ 'en_jenny': 'en-US-AndrewMultilingualNeural',
825
+ # Portuguese - Multilingual (ONLY Thalita)
826
+ 'pt-br-thalitamultilingualneural': 'pt-BR-ThalitaMultilingualNeural',
827
+ 'thalita': 'pt-BR-ThalitaMultilingualNeural',
828
+ 'pt_thalita': 'pt-BR-ThalitaMultilingualNeural',
829
+ 'pt_br_thalita': 'pt-BR-ThalitaMultilingualNeural',
830
+ 'pt': 'pt-BR-ThalitaMultilingualNeural',
831
+ 'pt_francisco': 'pt-BR-ThalitaMultilingualNeural',
832
+ # French - Multilingual
833
+ 'fr-fr-viviennemultilingualneural': 'fr-FR-VivienneMultilingualNeural',
834
+ 'fr-fr-remymultilingualneural': 'fr-FR-RemyMultilingualNeural',
835
+ 'denise': 'fr-FR-VivienneMultilingualNeural',
836
+ 'fr': 'fr-FR-VivienneMultilingualNeural',
837
+ 'fr_denise': 'fr-FR-VivienneMultilingualNeural',
838
+ # German - Multilingual
839
+ 'de-de-seraphinamultilingualneural': 'de-DE-SeraphinaMultilingualNeural',
840
+ 'de-de-florianmultilingualneural': 'de-DE-FlorianMultilingualNeural',
841
+ 'katja': 'de-DE-SeraphinaMultilingualNeural',
842
+ 'de': 'de-DE-SeraphinaMultilingualNeural',
843
+ 'de_katja': 'de-DE-SeraphinaMultilingualNeural',
844
+ # Korean - Multilingual (Hyunsu, NOT SunHee)
845
+ 'ko-kr-hyusumultilingualneural': 'ko-KR-HyunsuMultilingualNeural',
846
+ 'ko-kr-hyunsuneural': 'ko-KR-HyunsuMultilingualNeural',
847
+ 'sunhee': 'ko-KR-HyunsuMultilingualNeural',
848
+ 'ko': 'ko-KR-HyunsuMultilingualNeural',
849
+ 'ko_sunhee': 'ko-KR-HyunsuMultilingualNeural',
850
+ # Italian - Multilingual
851
+ 'it-it-giuseppemultilingualneural': 'it-IT-GiuseppeMultilingualNeural',
852
+ # Spanish (keep for backward compat)
853
+ 'ela': 'en-US-AndrewMultilingualNeural',
854
+ 'es_ela': 'en-US-AndrewMultilingualNeural',
855
+ 'es': 'en-US-AndrewMultilingualNeural',
856
+ 'es_carlos': 'en-US-AndrewMultilingualNeural',
857
+ # Japanese (keep for backward compat)
858
+ 'nanami': 'en-US-AndrewMultilingualNeural',
859
+ 'ja': 'en-US-AndrewMultilingualNeural',
860
+ 'ja_nanami': 'en-US-AndrewMultilingualNeural',
861
+ # Chinese (keep for backward compat)
862
+ 'xiaochen': 'en-US-AndrewMultilingualNeural',
863
+ 'zh': 'en-US-AndrewMultilingualNeural',
864
+ 'zh_xiaochen': 'en-US-AndrewMultilingualNeural',
865
+ }.get(voice, 'vi-VN-HoaiMyNeural')
866
+ part_files=[]
867
  for idx, seg in enumerate(segments):
868
+ frame=os.path.join(work,f'frame_{idx:02d}.jpg')
869
+ aud=os.path.join(work,f'voice_{idx:02d}.mp3')
870
+ aud_fast=os.path.join(work,f'voice_{idx:02d}_fast.mp3')
871
+ part=os.path.join(work,f'part_{idx:02d}.mp4')
872
  _make_scene_frame(post, seg, idx, len(segments), img, frame, emotion=emotion)
873
+ spoken=_emotion_script(seg, emotion)
874
  try:
875
+ subprocess.run(['python','-m','edge_tts','--voice',edge_voice,'--text',spoken,'--write-media',aud], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
876
  except Exception:
877
+ tld='com.vn' if voice in ('nu','female','mien-nam','hoaimy') else 'com'
878
  try:
879
  base.gTTS(spoken, lang='vi', tld=tld, slow=False).save(aud)
880
  except TypeError:
881
  base.gTTS(spoken, lang='vi', slow=False).save(aud)
882
+ subprocess.run(['ffmpeg','-y','-i',aud,'-filter:a',f'atempo={speed}','-vn',aud_fast], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=90)
883
+ dur=_estimate_audio_duration(aud_fast, fallback=15.0)+0.35
884
+ subprocess.run(['ffmpeg','-y','-loop','1','-t',str(dur),'-i',frame,'-i',aud_fast,'-shortest','-c:v','libx264','-tune','stillimage','-pix_fmt','yuv420p','-c:a','aac','-b:a','128k',part], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=150)
885
  part_files.append(part)
886
+ concat=os.path.join(work,'concat.txt')
887
+ with open(concat,'w',encoding='utf-8') as f:
888
  for p in part_files:
889
  f.write("file '" + p.replace("'", "'\\''") + "'\n")
890
+ subprocess.run(['ffmpeg','-y','-f','concat','-safe','0','-i',concat,'-c','copy',out_mp4], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=180)
891
  post['video'] = '/api/ai/short-file/' + post_id + suffix
892
  post['short_voice'] = voice
893
  post['short_emotion'] = emotion
 
895
  post['short_segments'] = segments
896
  post['short_subtitles'] = False
897
  base._save_ai_wall(posts)
898
+ return JSONResponse({'video': post['video'], 'voice': voice, 'emotion': emotion, 'speed': speed, 'subtitles': False, 'segments': segments})
899
  except Exception as e:
900
+ return JSONResponse({'error': 'Không tạo được shorts: ' + str(e)[:220]}, status_code=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
901
 
902
 
903
  @app.get('/api/ai/short-file/{file_id}')
 
914
  return JSONResponse({'posts': posts[:80]})
915
 
916
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917
  app.router.routes = [r for r in app.router.routes if not (getattr(r, 'path', None) == '/' and 'GET' in getattr(r, 'methods', set()))]