import gradio as gr from huggingface_hub import InferenceClient, HfApi import os import requests from typing import List, Dict, Union, Tuple import traceback from PIL import Image from io import BytesIO import asyncio from gradio_client import Client import time import threading import json import re # HuggingFace 관련 API 키 (스페이스 분석 용) HF_TOKEN = os.getenv("HF_TOKEN") hf_api = HfApi(token=HF_TOKEN) # Gemini 2.0 Thinking 모델 관련 API 키 및 클라이언트 (LLM 용) G_API_KEY = os.getenv("G_API_KEY") gemini_client = InferenceClient("Gemini-2.0-thinking", token=G_API_KEY) def get_headers(): if not HF_TOKEN: raise ValueError("Hugging Face token not found in environment variables") return {"Authorization": f"Bearer {HF_TOKEN}"} def get_file_content(space_id: str, file_path: str) -> str: file_url = f"https://huggingface.co/spaces/{space_id}/raw/main/{file_path}" try: response = requests.get(file_url, headers=get_headers()) if response.status_code == 200: return response.text else: return f"File not found or inaccessible: {file_path}" except requests.RequestException: return f"Error fetching content for file: {file_path}" def get_space_structure(space_id: str) -> Dict: try: files = hf_api.list_repo_files(repo_id=space_id, repo_type="space") tree = {"type": "directory", "path": "", "name": space_id, "children": []} for file in files: path_parts = file.split('/') current = tree for i, part in enumerate(path_parts): if i == len(path_parts) - 1: # 파일 current["children"].append({"type": "file", "path": file, "name": part}) else: # 디렉토리 found = False for child in current["children"]: if child["type"] == "directory" and child["name"] == part: current = child found = True break if not found: new_dir = {"type": "directory", "path": '/'.join(path_parts[:i+1]), "name": part, "children": []} current["children"].append(new_dir) current = new_dir return tree except Exception as e: print(f"Error in get_space_structure: {str(e)}") return {"error": f"API request error: {str(e)}"} def format_tree_structure(tree_data: Dict, indent: str = "") -> str: if "error" in tree_data: return tree_data["error"] formatted = f"{indent}{'📁' if tree_data.get('type') == 'directory' else '📄'} {tree_data.get('name', 'Unknown')}\n" if tree_data.get("type") == "directory": for child in sorted(tree_data.get("children", []), key=lambda x: (x.get("type", "") != "directory", x.get("name", ""))): formatted += format_tree_structure(child, indent + " ") return formatted def summarize_code(app_content: str): system_message = "당신은 Python 코드를 분석하고 요약하는 AI 조수입니다. 주어진 코드를 3줄 이내로 간결하게 요약해주세요." user_message = f"다음 Python 코드를 3줄 이내로 요약해주세요:\n\n{app_content}" messages = [ {"role": "system", "content": system_message}, {"role": "user", "content": user_message} ] try: response = gemini_client.chat_completion(messages, max_tokens=200, temperature=0.7) return response.choices[0].message.content except Exception as e: return f"요약 생성 중 오류 발생: {str(e)}" def analyze_code(app_content: str): system_message = """당신은 Python 코드를 분석하는 AI 조수입니다. 주어진 코드를 분석하여 다음 항목에 대해 설명해주세요: A. 배경 및 필요성 B. 기능적 효용성 및 가치 C. 특장점 D. 적용 대상 및 타겟 E. 기대효과 기존 및 유사 프로젝트와 비교하여 분석해주세요. Markdown 형식으로 출력하세요.""" user_message = f"다음 Python 코드를 분석해주세요:\n\n{app_content}" messages = [ {"role": "system", "content": system_message}, {"role": "user", "content": user_message} ] try: response = gemini_client.chat_completion(messages, max_tokens=1000, temperature=0.7) return response.choices[0].message.content except Exception as e: return f"분석 생성 중 오류 발생: {str(e)}" def explain_usage(app_content: str): system_message = "당신은 Python 코드를 분석하여 사용법을 설명하는 AI 조수입니다. 주어진 코드를 바탕으로 마치 화면을 보는 것처럼 사용법을 상세히 설명해주세요. Markdown 형식으로 출력하세요." user_message = f"다음 Python 코드의 사용법을 설명해주세요:\n\n{app_content}" messages = [ {"role": "system", "content": system_message}, {"role": "user", "content": user_message} ] try: response = gemini_client.chat_completion(messages, max_tokens=800, temperature=0.7) return response.choices[0].message.content except Exception as e: return f"사용법 설명 생성 중 오류 발생: {str(e)}" def adjust_lines_for_code(code_content: str, min_lines: int = 10, max_lines: int = 100) -> int: """ 코드 내용에 따라 lines 수를 동적으로 조정합니다. Parameters: - code_content (str): 코드 텍스트 내용 - min_lines (int): 최소 lines 수 - max_lines (int): 최대 lines 수 Returns: - int: 설정된 lines 수 """ # 코드의 줄 수 계산 num_lines = len(code_content.split('\n')) # 줄 수가 min_lines보다 적다면 min_lines 사용, max_lines보다 크면 max_lines 사용 return min(max(num_lines, min_lines), max_lines) def analyze_space(url: str, progress=gr.Progress()): try: space_id = url.split('spaces/')[-1] # Space ID 유효성 검사 수정 if not re.match(r'^[\w.-]+/[\w.-]+$', space_id): raise ValueError(f"Invalid Space ID format: {space_id}") progress(0.1, desc="파일 구조 분석 중...") tree_structure = get_space_structure(space_id) if "error" in tree_structure: raise ValueError(tree_structure["error"]) tree_view = format_tree_structure(tree_structure) progress(0.3, desc="app.py 내용 가져오는 중...") app_content = get_file_content(space_id, "app.py") progress(0.5, desc="코드 요약 중...") summary = summarize_code(app_content) progress(0.7, desc="코드 분석 중...") analysis = analyze_code(app_content) progress(0.9, desc="사용법 설명 생성 중...") usage = explain_usage(app_content) # 줄 수 계산하여 lines 설정 app_py_lines = adjust_lines_for_code(app_content) progress(1.0, desc="완료") return app_content, tree_view, tree_structure, space_id, summary, analysis, usage, app_py_lines except Exception as e: print(f"Error in analyze_space: {str(e)}") print(traceback.format_exc()) return f"오류가 발생했습니다: {str(e)}", "", None, "", "", "", "", 10 def respond( message: str, history: List[Tuple[str, str]], system_message: str = "", max_tokens: int = 1024, temperature: float = 0.7, top_p: float = 0.9, ): system_prefix = """당신은 허깅페이스에 특화된 AI 코딩 전문가입니다. 사용자의 질문에 친절하고 상세하게 답변해주세요. Gradio 특성을 정확히 인식하고 Requirements.txt 누락없이 코딩과 오류를 해결해야 합니다. 항상 정확하고 유용한 정보를 제공하도록 노력하세요.""" messages = [{"role": "system", "content": f"{system_prefix} {system_message}"}] for user_msg, assistant_msg in history: messages.append({"role": "user", "content": user_msg}) if assistant_msg: messages.append({"role": "assistant", "content": assistant_msg}) messages.append({"role": "user", "content": message}) response = "" for message in gemini_client.chat_completion( messages, max_tokens=max_tokens, stream=True, temperature=temperature, top_p=top_p, ): token = message.choices[0].delta.get('content', None) if token: response += token.strip("") yield response def create_ui(): try: css = """ /* 전체 배경 및 기본 글꼴 설정 */ body { background-color: #f9f9f9; font-family: 'Helvetica Neue', Arial, sans-serif; color: #333; } /* 하단 푸터 숨김 */ footer { visibility: hidden; } /* 출력 그룹 스타일: 밝은 배경, 부드러운 테두리와 그림자 */ .output-group { border: 1px solid #ccc; border-radius: 8px; padding: 15px; margin-bottom: 20px; background-color: #ffffff; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } /* 스크롤 영역 설정 */ .scroll-lock { overflow-y: auto !important; max-height: 300px !important; } .tree-view-scroll { overflow-y: auto !important; max-height: 400px !important; } .full-height { height: 80vh !important; overflow-y: auto !important; } /* 코드 박스 스타일: 모노스페이스 폰트와 밝은 배경 */ .code-box { overflow-x: auto !important; overflow-y: auto !important; white-space: pre !important; background-color: #f5f5f5; border-radius: 4px; padding: 10px; font-family: 'Courier New', Courier, monospace; } .code-box > div { min-width: 100% !important; } .code-box > div > textarea { word-break: normal !important; overflow-wrap: normal !important; } /* 탭 내비게이션 스타일: 단순하고 깔끔한 디자인 */ .tab-nav { background-color: #ffffff; border-bottom: 1px solid #ccc; display: flex; } .tab-nav button { background: none; border: none; padding: 10px 20px; margin: 0; cursor: pointer; font-size: 16px; color: #555; transition: color 0.3s, border-bottom 0.3s; } .tab-nav button:hover, .tab-nav button.selected { color: #000; border-bottom: 2px solid #007BFF; } /* 입력창 및 텍스트 영역 스타일 */ input[type="text"], textarea { color: #333; background-color: #fff; border: 1px solid #ccc; border-radius: 4px; padding: 8px; } """ with gr.Blocks(theme="default", css=css) as demo: gr.Markdown("# MOUSE: HF Space Deep-Research", elem_classes="header-markdown") with gr.Tabs() as tabs: with gr.TabItem("분석"): with gr.Row(): with gr.Column(scale=6): # 왼쪽 영역 url_input = gr.Textbox(label="HuggingFace Space URL", placeholder="예: https://huggingface.co/spaces/username/space_name") analyze_button = gr.Button("분석", variant="primary") with gr.Group(elem_classes="output-group scroll-lock"): summary_output = gr.Markdown(label="요약 (3줄 이내)") with gr.Group(elem_classes="output-group scroll-lock"): analysis_output = gr.Markdown(label="분석") with gr.Group(elem_classes="output-group scroll-lock"): usage_output = gr.Markdown(label="사용법") with gr.Group(elem_classes="output-group tree-view-scroll"): tree_view_output = gr.Textbox(label="파일 구조 (Tree View)", lines=30) with gr.Column(scale=4): # 오른쪽 영역 with gr.Group(elem_classes="output-group full-height"): code_tabs = gr.Tabs() with code_tabs: with gr.TabItem("app.py"): app_py_content = gr.Code( language="python", label="app.py", lines=200, elem_classes="full-height code-box" ) with gr.TabItem("requirements.txt"): requirements_content = gr.Textbox( label="requirements.txt", lines=200, elem_classes="full-height code-box" ) with gr.TabItem("AI 코딩"): chatbot = gr.Chatbot(label="대화", elem_classes="output-group full-height") msg = gr.Textbox(label="메시지", placeholder="메시지를 입력하세요...") # 숨겨진 파라미터 max_tokens = gr.Slider(minimum=1, maximum=8000, value=4000, label="Max Tokens", visible=False) temperature = gr.Slider(minimum=0, maximum=1, value=0.7, label="Temperature", visible=False) top_p = gr.Slider(minimum=0, maximum=1, value=0.9, label="Top P", visible=False) examples = [ ["상세한 사용 방법을 마치 화면을 보면서 설명하듯이 4000 토큰 이상 자세히 설명하라"], ["FAQ 20건을 상세하게 작성하라. 4000토큰 이상 사용하라."], ["사용 방법과 차별점, 특징, 강점을 중심으로 4000 토큰 이상 유튜브 영상 스크립트 형태로 작성하라"], ["본 서비스를 SEO 최적화하여 블로그 포스트로 4000 토큰 이상 작성하라"], ["특허 출원에 활용할 혁신적인 창의 발명 내용을 중심으로 4000 토큰 이상 작성하라."], ["계속 이어서 답변하라"], ] gr.Examples(examples, inputs=msg) def respond_wrapper(message, chat_history, max_tokens, temperature, top_p): bot_message = "" for response in respond(message, chat_history, max_tokens=max_tokens, temperature=temperature, top_p=top_p): bot_message = response # 마지막 응답 저장 yield "", chat_history + [(message, bot_message)] chat_history.append((message, bot_message)) return "", chat_history msg.submit(respond_wrapper, [msg, chatbot, max_tokens, temperature, top_p], [msg, chatbot]) with gr.TabItem("Recommended Best"): gr.Markdown( "Discover the best recommended HuggingFace Spaces [here](https://huggingface.co/spaces/openfree/Korean-Leaderboard).", elem_id="recommended-best" ) # 상태 저장용 변수 space_id_state = gr.State() tree_structure_state = gr.State() app_py_content_lines = gr.State() analyze_button.click( analyze_space, inputs=[url_input], outputs=[app_py_content, tree_view_output, tree_structure_state, space_id_state, summary_output, analysis_output, usage_output, app_py_content_lines] ).then( lambda space_id: get_file_content(space_id, "requirements.txt"), inputs=[space_id_state], outputs=[requirements_content] ) # 동적으로 app.py의 줄 수 조정 app_py_content.change(lambda lines: gr.update(lines=lines), inputs=[app_py_content_lines], outputs=[app_py_content]) return demo except Exception as e: print(f"Error in create_ui: {str(e)}") print(traceback.format_exc()) raise if __name__ == "__main__": try: print("Starting HuggingFace Space Analyzer...") demo = create_ui() print("UI created successfully.") print("Configuring Gradio queue...") demo.queue() print("Gradio queue configured.") print("Launching Gradio app...") demo.launch( server_name="0.0.0.0", server_port=7860, share=False, debug=True, show_api=False ) print("Gradio app launched successfully.") except Exception as e: print(f"Error in main: {str(e)}") print("Detailed error information:") print(traceback.format_exc()) raise