import os
import random
import base64
import requests
import tempfile
import shutil
import time
import numpy as np
from typing import List, Tuple
from datetime import datetime, timedelta
from pathlib import Path
from io import BytesIO
from urllib.parse import urljoin
# Selenium 관련
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import WebDriverException, TimeoutException
# 이미지 처리
from PIL import Image
# Gradio
import gradio as gr
# HuggingFace
from huggingface_hub import InferenceClient
from dotenv import load_dotenv
# HTML 파싱
from bs4 import BeautifulSoup
# 음성 및 비디오 처리
from gtts import gTTS
from moviepy.editor import (
VideoFileClip,
AudioFileClip,
ImageClip,
TextClip,
concatenate_videoclips
)
# MoviePy 설정
from moviepy.config import change_settings
change_settings({"IMAGEMAGICK_BINARY": "convert"}) # ImageMagick 바이너리 설정
# .env 파일에서 환경 변수 로드
load_dotenv()
# HuggingFace 인퍼런스 클라이언트 설정
hf_client = InferenceClient(
"CohereForAI/c4ai-command-r-plus-08-2024",
token=os.getenv("HF_TOKEN")
)
# 스크린샷 캐시 디렉토리 설정
CACHE_DIR = Path("screenshot_cache")
CACHE_DIR.mkdir(exist_ok=True)
# 전역 변수로 스크린샷 캐시 선언
SCREENSHOT_CACHE = {}
def get_cached_screenshot(url: str) -> str:
"""캐시된 스크린샷 가져오기 또는 새로 생성"""
try:
# URL을 안전한 파일명으로 변환
safe_filename = base64.urlsafe_b64encode(url.encode()).decode()
cache_file = CACHE_DIR / f"{safe_filename[:200]}.jpg" # PNG 대신 JPG 사용
if cache_file.exists():
try:
with Image.open(cache_file) as img:
buffered = BytesIO()
img.save(buffered, format="JPEG", quality=85, optimize=True)
return base64.b64encode(buffered.getvalue()).decode()
except Exception as e:
print(f"Cache read error for {url}: {e}")
if cache_file.exists():
cache_file.unlink()
return take_screenshot(url)
except Exception as e:
print(f"Screenshot cache error for {url}: {e}")
return ""
def take_screenshot(url: str) -> str:
"""웹사이트 스크린샷 촬영"""
if not url.startswith('http'):
url = f"https://{url}"
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--window-size=1080,720')
driver = None
try:
driver = webdriver.Chrome(options=options)
driver.get(url)
# 페이지 로딩 대기
WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
# 추가 대기 시간
time.sleep(3)
# 스크린샷 촬영 및 최적화
screenshot = driver.get_screenshot_as_png()
img = Image.open(BytesIO(screenshot))
# 이미지 크기 최적화
max_size = (800, 600)
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# JPEG로 변환 및 최적화
if img.mode in ('RGBA', 'LA'):
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[-1])
img = background
# 캐시 저장
safe_filename = base64.urlsafe_b64encode(url.encode()).decode()
cache_file = CACHE_DIR / f"{safe_filename[:200]}.jpg"
img.save(cache_file, format="JPEG", quality=85, optimize=True)
# 반환용 이미지 생성
buffered = BytesIO()
img.save(buffered, format="JPEG", quality=85, optimize=True)
return base64.b64encode(buffered.getvalue()).decode()
except Exception as e:
print(f"Screenshot error for {url}: {e}")
return ""
finally:
if driver:
driver.quit()
def cleanup_cache():
"""캐시 정리"""
try:
current_time = time.time()
for cache_file in CACHE_DIR.glob("*.jpg"):
try:
# 24시간 이상 된 파일 또는 0바이트 파일 삭제
if (current_time - cache_file.stat().st_mtime > 86400) or cache_file.stat().st_size == 0:
cache_file.unlink()
except Exception as e:
print(f"Error cleaning cache file {cache_file}: {e}")
except Exception as e:
print(f"Cache cleanup error: {e}")
# 앱 시작 시 캐시 정리
cleanup_cache()
def calculate_rising_rate(created_date: str, rank: int) -> int:
"""AI Rising Rate 계산"""
# 생성일 기준 점수 계산
created = datetime.strptime(created_date.split('T')[0], '%Y-%m-%d')
today = datetime.now()
days_diff = (today - created).days
date_score = max(0, 300 - days_diff) # 최대 300점
# 순위 기준 점수 계산
rank_score = max(0, 600 - rank) # 최대 300점
# 총점 계산
total_score = date_score + rank_score
# 별 개수 계산 (0~5)
if total_score <= 200:
stars = 1
elif total_score <= 400:
stars = 2
elif total_score <= 600:
stars = 3
elif total_score <= 800:
stars = 4
else:
stars = 5
return stars
def get_popularity_grade(likes: int, stars: int) -> tuple:
"""AI Popularity Score 등급 계산"""
# 기본 점수 (likes)
base_score = min(likes, 10000) # 최대 10000점
# 별점 추가 점수 (별 하나당 500점)
star_score = stars * 1000
# 총점
total_score = base_score + star_score
# 등급 테이블 (18단계)
grades = [
(14500, "AAA+"), (14000, "AAA"), (13500, "AAA-"),
(13000, "AA+"), (12500, "AA"), (12000, "AA-"),
(11500, "A+"), (11000, "A"), (10000, "A-"),
(9000, "BBB+"), (8000, "BBB"), (7000, "BBB-"),
(6000, "BB+"), (5000, "BB"), (4000, "BB-"),
(3000, "B+"), (2000, "B"), (1000, "B-")
]
for threshold, grade in grades:
if total_score >= threshold:
return grade, total_score
return "B-", total_score
# get_card 함수 내의 hardware_info 부분을 다음으로 교체:
def get_rating_info(item: dict, index: int) -> str:
"""평가 정보 HTML 생성"""
created = item.get('createdAt', '').split('T')[0]
likes = int(str(item.get('likes', '0')).replace(',', ''))
# AI Rising Rate 계산
stars = calculate_rising_rate(created, index + 1)
star_html = "★" * stars + "☆" * (5 - stars) # 채워진 별과 빈 별 조합
# AI Popularity Score 계산
grade, score = get_popularity_grade(likes, stars)
# 등급별 색상 설정
grade_colors = {
'AAA': '#FFD700', 'AA': '#FFA500', 'A': '#FF4500',
'BBB': '#4169E1', 'BB': '#1E90FF', 'B': '#00BFFF'
}
grade_base = grade.rstrip('+-')
grade_color = grade_colors.get(grade_base, '#666666')
return f"""
AI Rising Rate:
{star_html}
AI Popularity Score:
{grade} ({score:,})
"""
def get_hardware_info(item: dict) -> tuple:
"""하드웨어 정보 추출"""
try:
# runtime 정보 확인
runtime = item.get('runtime', {})
# CPU 정보 처리
cpu_info = runtime.get('cpu', 'Standard')
# GPU 정보 처리
gpu_info = "None"
if runtime.get('accelerator') == "gpu":
gpu_type = runtime.get('gpu', {}).get('name', '')
gpu_memory = runtime.get('gpu', {}).get('memory', '')
if gpu_type:
gpu_info = f"{gpu_type}"
if gpu_memory:
gpu_info += f" ({gpu_memory}GB)"
# spaces decorator 확인
if '@spaces.GPU' in str(item.get('sdk_version', '')):
if gpu_info == "None":
gpu_info = "GPU Enabled"
# SDK 정보 처리
sdk = item.get('sdk', 'N/A')
print(f"Debug - Runtime Info: {runtime}") # 디버그 출력
print(f"Debug - GPU Info: {gpu_info}") # 디버그 출력
return cpu_info, gpu_info, sdk
except Exception as e:
print(f"Error parsing hardware info: {str(e)}")
return 'Standard', 'None', 'N/A'
def get_card(item: dict, index: int, card_type: str = "space") -> str:
"""통합 카드 HTML 생성"""
item_id = item.get('id', '')
author, title = item_id.split('/', 1)
likes = format(item.get('likes', 0), ',')
created = item.get('createdAt', '').split('T')[0]
# short_description 가져오기
short_description = item.get('cardData', {}).get('short_description', '')
# URL 정의
if card_type == "space":
url = f"https://huggingface.co/spaces/{item_id}"
elif card_type == "model":
url = f"https://huggingface.co/{item_id}"
else: # dataset
url = f"https://huggingface.co/datasets/{item_id}"
# 메타데이터 처리
tags = item.get('tags', [])
pipeline_tag = item.get('pipeline_tag', '')
license = item.get('license', '')
sdk = item.get('sdk', 'N/A')
# AI Rating 정보 가져오기
rating_info = get_rating_info(item, index)
# 카드 타입별 그라데이션 설정
if card_type == "space":
gradient_colors = """
rgba(255, 182, 193, 0.7), /* 파스텔 핑크 */
rgba(173, 216, 230, 0.7), /* 파스텔 블루 */
rgba(255, 218, 185, 0.7) /* 파스텔 피치 */
"""
bg_content = f"""
background-image: url(data:image/png;base64,{get_cached_screenshot(url) if get_cached_screenshot(url) else ''});
background-size: cover;
background-position: center;
"""
type_icon = "🎯"
type_label = "SPACE"
elif card_type == "model":
gradient_colors = """
rgba(110, 142, 251, 0.7), /* 모델 블루 */
rgba(130, 158, 251, 0.7),
rgba(150, 174, 251, 0.7)
"""
bg_content = f"""
background: linear-gradient(135deg, #6e8efb, #4a6cf7);
padding: 15px;
"""
type_icon = "🤖"
type_label = "MODEL"
else: # dataset
gradient_colors = """
rgba(255, 107, 107, 0.7), /* 데이터셋 레드 */
rgba(255, 127, 127, 0.7),
rgba(255, 147, 147, 0.7)
"""
bg_content = f"""
background: linear-gradient(135deg, #ff6b6b, #ff8787);
padding: 15px;
"""
type_icon = "📊"
type_label = "DATASET"
content_bg = f"""
background: linear-gradient(135deg, {gradient_colors});
backdrop-filter: blur(10px);
"""
# 태그 표시 (models와 datasets용)
tags_html = ""
if card_type != "space":
tags_html = f"""
{' '.join([f'''
#{tag}
''' for tag in tags[:5]])}
"""
# 카드 HTML 반환
return f"""
#{index + 1}
{type_icon} {type_label}
{tags_html}
{title}
{f'''
{short_description}
''' if card_type == "space" and short_description else ''}
👤 {author}
❤️ {likes}
📅 {created}
{rating_info}
"""
def get_trending_spaces(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
"""트렌딩 스페이스 가져오기"""
url = "https://huggingface.co/api/spaces"
try:
progress(0, desc="Fetching spaces data...")
params = {
'full': 'true',
'limit': 24
}
response = requests.get(url, params=params)
response.raise_for_status()
spaces = response.json()
# 검색어로 필터링
if search_query:
spaces = [space for space in spaces if search_query.lower() in
(space.get('id', '') + ' ' + space.get('title', '')).lower()]
# 정렬
sort_by = sort_by.lower()
if sort_by == "rising_rate":
spaces.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
elif sort_by == "popularity":
spaces.sort(key=lambda x: get_popularity_grade(
int(str(x.get('likes', '0')).replace(',', '')),
calculate_rising_rate(x.get('createdAt', ''), 0))[1],
reverse=True)
progress(0.1, desc="Creating gallery...")
html_content = """
"""
for idx, space in enumerate(spaces):
html_content += get_card(space, idx, "space")
progress((0.1 + 0.9 * idx/len(spaces)), desc=f"Loading space {idx+1}/{len(spaces)}...")
html_content += "
"
progress(1.0, desc="Complete!")
return html_content, f"Found {len(spaces)} spaces"
except Exception as e:
error_html = f'Error: {str(e)}
'
return error_html, f"Error: {str(e)}"
def get_models(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
"""인기 모델 가져오기"""
url = "https://huggingface.co/api/models"
try:
progress(0, desc="Fetching models data...")
params = {
'full': 'true',
'limit': 300
}
response = requests.get(url, params=params)
response.raise_for_status()
models = response.json()
# 검색어로 필터링
if search_query:
models = [model for model in models if search_query.lower() in
(model.get('id', '') + ' ' + model.get('title', '')).lower()]
# 정렬
sort_by = sort_by.lower()
if sort_by == "rising_rate":
models.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
elif sort_by == "popularity":
models.sort(key=lambda x: get_popularity_grade(
int(str(x.get('likes', '0')).replace(',', '')),
calculate_rising_rate(x.get('createdAt', ''), 0))[1],
reverse=True)
progress(0.1, desc="Creating gallery...")
html_content = """
"""
for idx, model in enumerate(models):
html_content += get_card(model, idx, "model")
progress((0.1 + 0.9 * idx/len(models)), desc=f"Loading model {idx+1}/{len(models)}...")
html_content += "
"
progress(1.0, desc="Complete!")
return html_content, f"Found {len(models)} models"
except Exception as e:
error_html = f'Error: {str(e)}
'
return error_html, f"Error: {str(e)}"
def get_datasets(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
"""인기 데이터셋 가져오기"""
url = "https://huggingface.co/api/datasets"
try:
progress(0, desc="Fetching datasets data...")
params = {
'full': 'true',
'limit': 300
}
response = requests.get(url, params=params)
response.raise_for_status()
datasets = response.json()
# 검색어로 필터링
if search_query:
datasets = [dataset for dataset in datasets if search_query.lower() in
(dataset.get('id', '') + ' ' + dataset.get('title', '')).lower()]
# 정렬
sort_by = sort_by.lower()
if sort_by == "rising_rate":
datasets.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
elif sort_by == "popularity":
datasets.sort(key=lambda x: get_popularity_grade(
int(str(x.get('likes', '0')).replace(',', '')),
calculate_rising_rate(x.get('createdAt', ''), 0))[1],
reverse=True)
progress(0.1, desc="Creating gallery...")
html_content = """
"""
for idx, dataset in enumerate(datasets):
html_content += get_card(dataset, idx, "dataset")
progress((0.1 + 0.9 * idx/len(datasets)), desc=f"Loading dataset {idx+1}/{len(datasets)}...")
html_content += "
"
progress(1.0, desc="Complete!")
return html_content, f"Found {len(datasets)} datasets"
except Exception as e:
error_html = f'Error: {str(e)}
'
return error_html, f"Error: {str(e)}"
# 정렬 함수 추가
def sort_items(items, sort_by):
if sort_by == "rank":
return items # 이미 순위대로 정렬되어 있음
elif sort_by == "rising_rate":
return sorted(items, key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
elif sort_by == "popularity":
return sorted(items, key=lambda x: get_popularity_grade(int(str(x.get('likes', '0')).replace(',', '')),
calculate_rising_rate(x.get('createdAt', ''), 0))[1], reverse=True)
return items
# API 호출 함수 수정
def fetch_items(item_type, search_query="", sort_by="rank", limit=1000):
"""아이템 가져오기 (spaces/models/datasets)"""
base_url = f"https://huggingface.co/api/{item_type}"
params = {
'full': 'true',
'limit': limit,
'search': search_query
}
try:
response = requests.get(base_url, params=params)
response.raise_for_status()
items = response.json()
# 검색어로 필터링
if search_query:
items = [item for item in items if search_query.lower() in
(item.get('id', '') + item.get('title', '')).lower()]
# 정렬
items = sort_items(items, sort_by)
return items[:300] # 상위 300개만 반환
except Exception as e:
print(f"Error fetching items: {e}")
return []
def get_space_source(space_id: str) -> dict:
"""스페이스의 소스코드 가져오기"""
try:
# HuggingFace API를 통해 소스코드 가져오기
headers = {"Authorization": f"Bearer {os.getenv('HF_TOKEN')}"}
# app.py 시도
app_url = f"https://huggingface.co/spaces/{space_id}/raw/main/app.py"
app_response = requests.get(app_url, headers=headers)
# index.html 시도
index_url = f"https://huggingface.co/spaces/{space_id}/raw/main/index.html"
index_response = requests.get(index_url, headers=headers)
source = {
"app.py": app_response.text if app_response.status_code == 200 else "",
"index.html": index_response.text if index_response.status_code == 200 else ""
}
return source
except Exception as e:
print(f"Error fetching source for {space_id}: {str(e)}")
return {"app.py": "", "index.html": ""}
def analyze_space(space_info: dict) -> str:
"""스페이스 분석"""
try:
space_id = space_info.get('id', '')
url = f"https://huggingface.co/spaces/{space_id}"
# 소스코드 가져오기
source = get_space_source(space_id)
source_code = source["app.py"] or source["index.html"]
if not source_code:
return f"""
#{space_info.get('rank', '0')} {space_id}
소스코드를 가져올 수 없습니다.
"""
# LLM 분석 프롬프트
prompt = f"""
다음은 HuggingFace 스페이스({url})의 코드 또는 주석입니다:
```
{source_code[:4000]}
```
이 내용을 기반으로 다음 항목을 분석해주세요:
1. 개요: (한 줄로)
2. 요약: (한 줄로)
3. 특징 및 장점: (한 줄로)
4. 사용 대상: (한 줄로)
5. 사용 방법: (한 줄로)
6. 유사 서비스와의 차별점: (한 줄로)
각 항목은 실제 확인된 내용만 포함하여 한 줄로 작성하세요.
코드가 보안처리된 경우 주석을 기반으로 분석하세요.
"""
# LLM 분석 실행
messages = [
{"role": "system", "content": "소스코드 분석 전문가로서 실제 코드 내용만 기반으로 분석하세요."},
{"role": "user", "content": prompt}
]
response = hf_client.chat_completion(
messages,
max_tokens=3800,
temperature=0.3
)
analysis = response.choices[0].message.content
return f"""
#{space_info.get('rank', '0')} {space_id}
{analysis}
소스코드 미리보기
{source_code[:500]}...
"""
except Exception as e:
return f"분석 오류: {str(e)}
"
def analyze_top_spaces(progress=gr.Progress()) -> Tuple[str, str]:
"""상위 24개 스페이스 분석"""
try:
progress(0, desc="스페이스 데이터 가져오는 중...")
url = "https://huggingface.co/api/spaces"
response = requests.get(url, params={'full': 'true', 'limit': 24})
response.raise_for_status()
spaces = response.json()[:24]
# 상단 입력 박스와 기본 텍스트를 포함한 HTML 시작
html_content = """
"""
for idx, space in enumerate(spaces):
progress((idx + 1) / 24, desc=f"분석 중... {idx+1}/24")
try:
source = get_space_source(space['id'])
source_code = source["app.py"] or source["index.html"]
# 스페이스 ID에서 사용자명 제거하고 프로젝트명만 추출
project_name = space['id'].split('/')[-1]
prompt = f"""
다음 HuggingFace 스페이스를 유튜브 뉴스 리포트 형식으로 설명해주세요.
시작은 반드시 "오늘의 인기순위 {idx + 1}위인 {project_name}입니다."로 시작하고,
이어서 주요 기능, 특징, 활용방안을 2-3문장으로 자연스럽게 설명해주세요.
전체 길이는 3-4문장으로 제한하고, 설명은 뉴스 리포터처럼 명확하고 전문적으로 해주세요.
소스코드:
```
{source_code[:1500]}
```
"""
messages = [
{"role": "system", "content": "AI 기술 전문 뉴스 리포터입니다."},
{"role": "user", "content": prompt}
]
response = hf_client.chat_completion(
messages,
max_tokens=1000,
temperature=0.7
)
script = response.choices[0].message.content.strip()
html_content += f"""
"""
except Exception as e:
print(f"Error analyzing space {space['id']}: {e}")
html_content += f"""
순위 {idx + 1}위 분석 중 오류가 발생했습니다.
"""
html_content += "
"
return html_content, f"24개 스페이스 분석 완료"
except Exception as e:
error_msg = f"Error: {str(e)}"
return f"{error_msg}
", error_msg
def analyze_single_space(space: dict, source_code: str) -> str:
"""단일 스페이스 분석"""
try:
if not source_code:
return "소스코드를 가져올 수 없습니다."
prompt = f"""
다음 스페이스의 소스코드를 분석해주세요:
```
{source_code[:4000]}
```
다음 항목을 각각 한 줄로 요약해주세요:
1. 개요:
2. 요약:
3. 특징 및 장점:
4. 사용 대상:
5. 사용 방법:
6. 유사 서비스와의 차별점:
"""
messages = [
{"role": "system", "content": "소스코드 분석 전문가입니다."},
{"role": "user", "content": prompt}
]
response = hf_client.chat_completion(
messages,
max_tokens=3800,
temperature=0.3
)
return response.choices[0].message.content
except Exception as e:
return f"분석 중 오류 발생: {str(e)}"
def create_editable_space_analysis(progress=gr.Progress()) -> List[str]:
"""24개 스페이스 분석 텍스트 생성"""
try:
progress(0, desc="스페이스 데이터 가져오는 중...")
url = "https://huggingface.co/api/spaces"
response = requests.get(url, params={'full': 'true', 'limit': 24})
response.raise_for_status()
spaces = response.json()[:24]
analysis_texts = []
for idx, space in enumerate(spaces):
progress((idx + 1) / 24, desc=f"분석 중... {idx+1}/24")
try:
source = get_space_source(space['id'])
source_code = source["app.py"] or source["index.html"]
# 프로젝트명만 추출
project_name = space['id'].split('/')[-1]
prompt = f"""
다음 HuggingFace 스페이스를 분석하여 뉴스 리포트 형식으로 설명해주세요:
시작은 반드시 "오늘의 인기순위 {idx + 1}위인 {project_name}입니다."로 시작하고,
이어서 주요 기능, 특징, 활용방안을 자연스럽게 설명해주세요.
소스코드:
```
{source_code[:1500]}
```
"""
messages = [
{"role": "system", "content": "AI 기술 전문 뉴스 리포터입니다."},
{"role": "user", "content": prompt}
]
response = hf_client.chat_completion(
messages,
max_tokens=200,
temperature=0.7
)
analysis_texts.append(response.choices[0].message.content.strip())
except Exception as e:
analysis_texts.append(f"오늘의 인기순위 {idx + 1}위인 {project_name}입니다.")
return analysis_texts
except Exception as e:
return [f"순위 {i+1}위 분석을 준비중입니다." for i in range(24)]
def generate_video(texts: List[str], progress=gr.Progress()) -> str:
"""영상 생성"""
try:
# 임시 디렉토리 생성
temp_dir = tempfile.mkdtemp()
clips = []
# 작업 디렉토리를 임시 디렉토리로 변경
current_dir = os.getcwd()
os.chdir(temp_dir)
try:
# 인트로 생성
intro_text = texts[0]
intro_clip = TextClip(
txt=intro_text,
fontsize=30,
color='white',
bg_color='black',
size=(800, 600),
method='label' # 'caption' 대신 'label' 사용
).set_duration(10) # 인트로는 좀 더 길게
# 인트로 음성
intro_audio = gTTS(text=intro_text, lang='ko', slow=False)
intro_audio_path = os.path.join(temp_dir, "intro.mp3")
intro_audio.save(intro_audio_path)
intro_audio_clip = AudioFileClip(intro_audio_path)
# 오디오 길이에 맞춰 비디오 길이 조정
intro_clip = intro_clip.set_duration(intro_audio_clip.duration)
intro_clip = intro_clip.set_audio(intro_audio_clip)
clips.append(intro_clip)
# 각 스페이스별 클립 생성
for idx, text in enumerate(texts[1:], 1):
progress((idx / 24), desc=f"영상 생성 중... {idx}/24")
# 텍스트 클립 생성
text_clip = TextClip(
txt=text,
fontsize=25,
color='white',
bg_color='black',
size=(800, 600),
method='label' # 'caption' 대신 'label' 사용
).set_duration(5)
# 음성 생성
tts = gTTS(text=text, lang='ko', slow=False)
audio_path = os.path.join(temp_dir, f"audio_{idx}.mp3")
tts.save(audio_path)
audio_clip = AudioFileClip(audio_path)
# 오디오 길이에 맞춰 비디오 길이 조정
text_clip = text_clip.set_duration(audio_clip.duration)
video_clip = text_clip.set_audio(audio_clip)
clips.append(video_clip)
# 모든 클립 연결
final_clip = concatenate_videoclips(clips)
# MP4로 저장
output_path = os.path.join(temp_dir, "output_video.mp4")
final_clip.write_videofile(
output_path,
fps=24,
codec='libx264',
audio_codec='aac'
)
# 결과 파일을 현재 디렉토리로 복사
final_output = os.path.join(current_dir, "output_video.mp4")
shutil.copy2(output_path, final_output)
return final_output
finally:
# 작업 디렉토리 복원
os.chdir(current_dir)
except Exception as e:
print(f"Error generating video: {e}")
return ""
finally:
# 임시 파일 정리
if 'temp_dir' in locals():
try:
shutil.rmtree(temp_dir)
except Exception as e:
print(f"Error cleaning up temp files: {e}")
def create_interface():
with gr.Blocks(title="HuggingFace Trending Board", css="""
.search-sort-container {
background: linear-gradient(135deg, rgba(255,255,255,0.95), rgba(240,240,255,0.95));
border-radius: 15px;
padding: 20px;
margin: 10px 0;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
overflow: visible;
}
.search-box {
border: 2px solid #e1e1e1;
border-radius: 10px;
padding: 12px;
transition: all 0.3s ease;
background: linear-gradient(135deg, #ffffff, #f8f9ff);
width: 100%;
}
.search-box:focus {
border-color: #7b61ff;
box-shadow: 0 0 0 2px rgba(123,97,255,0.2);
background: linear-gradient(135deg, #ffffff, #f0f3ff);
}
.refresh-btn {
background: linear-gradient(135deg, #7b61ff, #6366f1);
color: white;
border: none;
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
width: 120px;
height: 80px !important;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
font-size: 1.2em !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.refresh-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.2);
background: linear-gradient(135deg, #8b71ff, #7376f1);
}
""") as interface:
gr.Markdown("""
# 🤗 HuggingFace Trending Board
Explore, search, and sort through the Shows Top 300 Trending spaces with AI Ratings
""")
with gr.Tabs() as tabs:
# Spaces 탭
with gr.Tab("🎯 Trending Spaces"):
with gr.Row(elem_classes="search-sort-container"):
with gr.Column(scale=2):
spaces_search = gr.Textbox(
label="🔍 Search Spaces",
placeholder="Enter keywords to search...",
elem_classes="search-box"
)
with gr.Column(scale=2):
spaces_sort = gr.Radio(
choices=["rank", "rising_rate", "popularity"],
value="rank",
label="Sort by",
interactive=True
)
with gr.Column(scale=1):
spaces_refresh_btn = gr.Button(
"🔄 Refresh",
variant="primary",
elem_classes="refresh-btn"
)
spaces_gallery = gr.HTML()
spaces_status = gr.Markdown("Loading...")
# Models 탭
with gr.Tab("🤖 Trending Models"):
with gr.Row(elem_classes="search-sort-container"):
with gr.Column(scale=2):
models_search = gr.Textbox(
label="🔍 Search Models",
placeholder="Enter keywords to search...",
elem_classes="search-box"
)
with gr.Column(scale=2):
models_sort = gr.Radio(
choices=["rank", "rising_rate", "popularity"],
value="rank",
label="Sort by",
interactive=True
)
with gr.Column(scale=1):
models_refresh_btn = gr.Button(
"🔄 Refresh",
variant="primary",
elem_classes="refresh-btn"
)
models_gallery = gr.HTML()
models_status = gr.Markdown("Loading...")
# Datasets 탭
with gr.Tab("📊 Trending Datasets"):
with gr.Row(elem_classes="search-sort-container"):
with gr.Column(scale=2):
datasets_search = gr.Textbox(
label="🔍 Search Datasets",
placeholder="Enter keywords to search...",
elem_classes="search-box"
)
with gr.Column(scale=2):
datasets_sort = gr.Radio(
choices=["rank", "rising_rate", "popularity"],
value="rank",
label="Sort by",
interactive=True
)
with gr.Column(scale=1):
datasets_refresh_btn = gr.Button(
"🔄 Refresh",
variant="primary",
elem_classes="refresh-btn"
)
datasets_gallery = gr.HTML()
datasets_status = gr.Markdown("Loading...")
# 분석 탭 수정
with gr.Tab("🔍 Top 24 Spaces Analysis"):
with gr.Row(elem_classes="search-sort-container"):
analysis_refresh_btn = gr.Button(
"🔄 Analyze All 24 Spaces",
variant="primary",
elem_classes="refresh-btn"
)
# 편집 가능한 인트로 텍스트
intro_text = gr.Textbox(
value="안녕하세요. 매일 글로벌 최신 AI 인기 트렌드 서비스를 알아보는 '데일리 AI 트렌딩' 뉴스입니다. 오늘의 허깅페이스 인기 순위 1위부터 24위까지, 분석과 핵심 내용을 살펴보겠습니다.",
label="인트로 텍스트",
lines=4
)
# 24개의 편집 가능한 텍스트 박스를 담을 컨테이너
with gr.Column(elem_id="analysis-container"):
analysis_boxes = [gr.Textbox(label=f"Space #{i+1}", lines=3) for i in range(24)]
analysis_status = gr.Markdown()
# 비디오 생성 섹션
with gr.Row():
generate_btn = gr.Button(
"🎬 영상 생성",
variant="primary",
size="lg"
)
video_output = gr.Video(label="생성된 영상")
# Event handlers
spaces_refresh_btn.click(
fn=get_trending_spaces,
inputs=[spaces_search, spaces_sort],
outputs=[spaces_gallery, spaces_status]
)
models_refresh_btn.click(
fn=get_models,
inputs=[models_search, models_sort],
outputs=[models_gallery, models_status]
)
datasets_refresh_btn.click(
fn=get_datasets,
inputs=[datasets_search, datasets_sort],
outputs=[datasets_gallery, datasets_status]
)
# 검색어 변경 시 자동 새로고침
spaces_search.change(
fn=get_trending_spaces,
inputs=[spaces_search, spaces_sort],
outputs=[spaces_gallery, spaces_status]
)
models_search.change(
fn=get_models,
inputs=[models_search, models_sort],
outputs=[models_gallery, models_status]
)
datasets_search.change(
fn=get_datasets,
inputs=[datasets_search, datasets_sort],
outputs=[datasets_gallery, datasets_status]
)
# 정렬 방식 변경 시 자동 새로고침
spaces_sort.change(
fn=get_trending_spaces,
inputs=[spaces_search, spaces_sort],
outputs=[spaces_gallery, spaces_status]
)
models_sort.change(
fn=get_models,
inputs=[models_search, models_sort],
outputs=[models_gallery, models_status]
)
datasets_sort.change(
fn=get_datasets,
inputs=[datasets_search, datasets_sort],
outputs=[datasets_gallery, datasets_status]
)
# 분석 탭 이벤트 핸들러
analysis_refresh_btn.click(
fn=on_analyze,
outputs=analysis_boxes
)
generate_btn.click(
fn=on_generate_video,
inputs=[intro_text] + analysis_boxes,
outputs=video_output
)
# 초기 데이터 로드
interface.load(
fn=get_trending_spaces,
inputs=[spaces_search, spaces_sort],
outputs=[spaces_gallery, spaces_status]
)
interface.load(
fn=get_models,
inputs=[models_search, models_sort],
outputs=[models_gallery, models_status]
)
interface.load(
fn=get_datasets,
inputs=[datasets_search, datasets_sort],
outputs=[datasets_gallery, datasets_status]
)
interface.load(
fn=on_analyze,
outputs=analysis_boxes
)
return interface
# 분석 및 비디오 생성 함수들
def on_analyze(progress=gr.Progress()):
"""분석 실행 및 텍스트박스 업데이트"""
try:
url = "https://huggingface.co/api/spaces"
response = requests.get(url, params={'full': 'true', 'limit': 24})
response.raise_for_status()
spaces = response.json()[:24]
analysis_texts = []
for idx, space in enumerate(spaces):
progress((idx + 1) / 24, desc=f"분석 중... {idx+1}/24")
try:
source = get_space_source(space['id'])
source_code = source["app.py"] or source["index.html"]
project_name = space['id'].split('/')[-1]
prompt = f"""
다음 HuggingFace 스페이스를 분석하여 뉴스 리포트 형식으로 설명해주세요:
시작은 반드시 "오늘의 인기순위 {idx + 1}위인 {project_name}입니다."로 시작하고,
이어서 주요 기능, 특징, 활용방안을 자연스럽게 설명해주세요.
응답은 반드시 300자 이내로 작성해주세요.
소스코드:
```
{source_code[:2000]}
```
"""
messages = [
{"role": "system", "content": "AI 기술 전문 뉴스 리포터입니다. 300자 이내로 명확하고 간단하게 설명합니다."},
{"role": "user", "content": prompt}
]
response = hf_client.chat_completion(
messages,
max_tokens=500, # 토큰 수 증가
temperature=0.7
)
analysis = response.choices[0].message.content.strip()
# 300자로 제한
analysis = analysis[:300] + "..." if len(analysis) > 300 else analysis
analysis_texts.append(analysis)
except Exception as e:
analysis_texts.append(f"오늘의 인기순위 {idx + 1}위인 {project_name}입니다.")
if len(analysis_texts) < 24:
analysis_texts.extend([f"순위 {i+1}위 분석을 준비중입니다." for i in range(len(analysis_texts), 24)])
return analysis_texts[:24]
except Exception as e:
return [f"순위 {i+1}위 분석을 준비중입니다." for i in range(24)]
def on_generate_video(intro, *texts, progress=gr.Progress()):
"""영상 생성"""
all_texts = [intro] + list(texts)
return generate_video(all_texts, progress)
if __name__ == "__main__":
try:
CACHE_DIR.mkdir(exist_ok=True)
cleanup_cache()
demo = create_interface()
demo.launch(
share=True,
inbrowser=True,
show_api=False,
max_threads=4
)
except Exception as e:
print(f"Application error: {e}")