Download app.py from teowu/Kimi-K2.5: direct link, hf CLI and curl.
- Browser
- Download file 20.2 kB
-
https://huggingface.co/spaces/teowu/Kimi-K2.5/resolve/main/app.py
- Command line
-
hf download hf://spaces/teowu/Kimi-K2.5/app.py
-
curl -L -o app.py https://huggingface.co/spaces/teowu/Kimi-K2.5/resolve/main/app.py
20.2 kB
| import base64 | |
| import os | |
| import uuid | |
| import time | |
| import gradio as gr | |
| from gradio_client import utils as client_utils | |
| from config import DEFAULT_SYS_PROMPT, MODEL, api_key, base_url | |
| from openai import OpenAI | |
| # Initialize OpenAI client | |
| client = OpenAI( | |
| api_key=api_key, | |
| base_url=base_url, | |
| ) | |
| def encode_file_to_base64(file_path): | |
| """Encode file to base64 data URL.""" | |
| with open(file_path, "rb") as file: | |
| mime_type = client_utils.get_mimetype(file_path) | |
| base64_data = base64.b64encode(file.read()).decode("utf-8") | |
| return f"data:{mime_type};base64,{base64_data}" | |
| def is_video_file(file_path): | |
| """Check if file is a video.""" | |
| video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv'} | |
| ext = os.path.splitext(file_path)[1].lower() | |
| return ext in video_extensions | |
| def is_image_file(file_path): | |
| """Check if file is an image.""" | |
| image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'} | |
| ext = os.path.splitext(file_path)[1].lower() | |
| return ext in image_extensions | |
| def format_messages(history, sys_prompt=None): | |
| """Format chat history to OpenAI message format.""" | |
| messages = [{ | |
| "role": "system", | |
| "content": sys_prompt or DEFAULT_SYS_PROMPT, | |
| }] | |
| for item in history: | |
| if item["role"] == "user": | |
| content_parts = [] | |
| # Add files (images and videos) | |
| if item.get("files"): | |
| for file_path in item["files"]: | |
| if file_path.startswith("http"): | |
| # URL - determine type by extension | |
| if is_video_file(file_path): | |
| content_parts.append({ | |
| "type": "video_url", | |
| "video_url": {"url": file_path} | |
| }) | |
| else: | |
| content_parts.append({ | |
| "type": "image_url", | |
| "image_url": {"url": file_path} | |
| }) | |
| elif os.path.exists(file_path): | |
| data_url = encode_file_to_base64(file_path) | |
| if is_video_file(file_path): | |
| content_parts.append({ | |
| "type": "video_url", | |
| "video_url": {"url": data_url} | |
| }) | |
| else: | |
| content_parts.append({ | |
| "type": "image_url", | |
| "image_url": {"url": data_url} | |
| }) | |
| # Add text | |
| if item.get("content"): | |
| content_parts.append({ | |
| "type": "text", | |
| "text": item.get("content", "") | |
| }) | |
| messages.append({ | |
| "role": "user", | |
| "content": content_parts if len(content_parts) > 1 else item.get("content", "") | |
| }) | |
| elif item["role"] == "assistant": | |
| content = item.get("content", "").strip() | |
| if content: | |
| messages.append({ | |
| "role": "assistant", | |
| "content": content | |
| }) | |
| return messages | |
| def chat_stream(messages, model, enable_thinking=True): | |
| """Stream chat completion from API.""" | |
| try: | |
| kwargs = { | |
| "model": model, | |
| "messages": messages, | |
| "stream": True, | |
| "top_p": 0.95, | |
| } | |
| if enable_thinking: | |
| kwargs["temperature"] = 1 | |
| kwargs["extra_body"] = {"thinking": {"type": "enabled"}} | |
| else: | |
| kwargs["temperature"] = 0.6 | |
| kwargs["extra_body"] = {"thinking": {"type": "disabled"}} | |
| print(f"[DEBUG] enable_thinking={enable_thinking}, model={model}") | |
| response = client.chat.completions.create(**kwargs) | |
| for chunk in response: | |
| if chunk and chunk.choices: | |
| delta = chunk.choices[0].delta | |
| if hasattr(delta, 'content') and delta.content: | |
| yield delta.content, None | |
| elif hasattr(delta, 'reasoning_content') and delta.reasoning_content: | |
| yield None, delta.reasoning_content | |
| except Exception as e: | |
| yield f"\n\n**Error:** {str(e)}", None | |
| # Global state | |
| chat_history = [] | |
| conversations = {} | |
| current_conversation_id = None | |
| enable_thinking = True | |
| uploaded_files = [] | |
| def create_new_conversation(): | |
| """Create a new conversation.""" | |
| global current_conversation_id, chat_history, enable_thinking, uploaded_files | |
| current_conversation_id = str(uuid.uuid4()) | |
| chat_history = [] | |
| uploaded_files = [] | |
| conversations[current_conversation_id] = { | |
| "history": chat_history, | |
| "enable_thinking": True | |
| } | |
| enable_thinking = True | |
| return [], None, gr.update(value="⚡ Thinking: ON") | |
| def toggle_thinking(): | |
| """Toggle thinking mode.""" | |
| global enable_thinking | |
| enable_thinking = not enable_thinking | |
| if current_conversation_id and current_conversation_id in conversations: | |
| conversations[current_conversation_id]["enable_thinking"] = enable_thinking | |
| if enable_thinking: | |
| return gr.update(value="⚡ Thinking: ON") | |
| else: | |
| return gr.update(value="💤 Thinking: OFF") | |
| def format_chat_display(history): | |
| """Format history for Gradio Chatbot display.""" | |
| messages = [] | |
| for item in history: | |
| if item["role"] == "user": | |
| content_parts = [] | |
| # Add files first | |
| if item.get("files"): | |
| for f in item["files"]: | |
| if is_image_file(f): | |
| content_parts.append({"type": "image", "path": f}) | |
| else: | |
| content_parts.append({"type": "text", "text": f"🎥 {os.path.basename(f)}"}) | |
| # Add text | |
| if item.get("content"): | |
| content_parts.append({"type": "text", "text": item["content"]}) | |
| if len(content_parts) == 1 and content_parts[0].get("type") == "text": | |
| messages.append({"role": "user", "content": content_parts[0]["text"]}) | |
| else: | |
| messages.append({"role": "user", "content": content_parts}) | |
| elif item["role"] == "assistant": | |
| messages.append({"role": "assistant", "content": item.get("content", "")}) | |
| return messages | |
| def handle_upload(files): | |
| """Handle file upload.""" | |
| global uploaded_files | |
| if files: | |
| if isinstance(files, list): | |
| for f in files: | |
| if isinstance(f, dict) and 'name' in f: | |
| uploaded_files.append(f['name']) | |
| elif isinstance(f, str): | |
| uploaded_files.append(f) | |
| elif isinstance(files, dict) and 'name' in files: | |
| uploaded_files.append(files['name']) | |
| elif isinstance(files, str): | |
| uploaded_files.append(files) | |
| # Return preview text | |
| if uploaded_files: | |
| preview = "📎 " + ", ".join([os.path.basename(p) for p in uploaded_files[-3:]]) | |
| if len(uploaded_files) > 3: | |
| preview += f" (+{len(uploaded_files) - 3} more)" | |
| return preview | |
| return "" | |
| def user_message(message, files): | |
| """Add user message to history.""" | |
| global chat_history, current_conversation_id, uploaded_files | |
| if not current_conversation_id: | |
| create_new_conversation() | |
| # Handle newly uploaded files | |
| if files: | |
| for f in files: | |
| if isinstance(f, str) and f not in uploaded_files: | |
| uploaded_files.append(f) | |
| chat_history.append({ | |
| "role": "user", | |
| "content": message, | |
| "files": uploaded_files.copy() | |
| }) | |
| conversations[current_conversation_id]["history"] = chat_history | |
| conversations[current_conversation_id]["enable_thinking"] = enable_thinking | |
| # Clear uploaded files after sending | |
| uploaded_files = [] | |
| return format_chat_display(chat_history), "", None | |
| def bot_response(): | |
| """Generate bot response.""" | |
| global chat_history | |
| if not chat_history or chat_history[-1]["role"] != "user": | |
| return format_chat_display(chat_history) | |
| messages = format_messages(chat_history) | |
| model = MODEL | |
| reasoning_text = "" | |
| answer_text = "" | |
| is_thinking = False | |
| assistant_msg = {"role": "assistant", "content": ""} | |
| chat_history.append(assistant_msg) | |
| try: | |
| for content, reasoning in chat_stream(messages, model, enable_thinking): | |
| if reasoning: | |
| reasoning_text += reasoning | |
| is_thinking = True | |
| if content: | |
| answer_text += content | |
| # Build response with thinking section | |
| final_response = "" | |
| if is_thinking and reasoning_text.strip(): | |
| if content: | |
| # Thinking done, show in collapsed details | |
| final_response += f"""<details style="margin-bottom: 12px;"> | |
| <summary style="font-size: 13px; cursor: pointer;">🧠 Thought for a moment...</summary> | |
| <div style="font-size: 12px; font-style: italic; padding: 8px 0; line-height: 1.6;"> | |
| {reasoning_text} | |
| </div> | |
| </details> | |
| """ | |
| else: | |
| # Still thinking, show directly | |
| final_response += f'<div style="font-size: 12px; font-style: italic; margin-bottom: 12px; line-height: 1.6;">{reasoning_text}</div>\n\n' | |
| final_response += answer_text | |
| assistant_msg["content"] = final_response | |
| yield format_chat_display(chat_history) | |
| conversations[current_conversation_id]["history"] = chat_history | |
| except Exception as e: | |
| if chat_history and chat_history[-1]["role"] == "assistant" and not chat_history[-1]["content"].strip(): | |
| chat_history.pop() | |
| raise e | |
| # Quick Load Examples | |
| QUICK_EXAMPLES = [ | |
| {"label": "📝 Longform Creative Writing", "prompt": "Write a 2000-word story about a cat who learns to fly. Make it whimsical, heartwarming, yet enough creative.", "file": None}, | |
| {"label": "💻 Python Concept Explanation", "prompt": "Explain how Python decorators work with simple examples.", "file": None}, | |
| {"label": "👀🦋 Recognize Butterfly Species", "prompt": "What species of butterfly is this? Describe its key characteristics.", "file": "butterfly.jpg"}, | |
| {"label": "🧠 Brainstorm Ideas", "prompt": "Brainstorm 10 creative marketing campaign ideas for a new eco-friendly water bottle.", "file": None}, | |
| {"label": "🌐 Translation and Analysis", "prompt": "Translate the following text to Chinese: 'Gatsby believed in the green light, the orgastic future that year by year recedes before us. It eluded us then, but that's no matter--tomorrow we will run faster, stretch out our arms farther.... And one fine morning-- So we beat on, boats against the current, borne back ceaselessly into the past.' and make a bilingual & deep explanation. Your explanation shall be in one English paragraph followed by one Chinese paragraph, interleaved.", "file": None}, | |
| {"label": "👀📊 Analyze Benchmarks", "prompt": "List all benchmarks where K2.5 reaches SOTA and make it a new barchart; In the barchart, take Opus and Gemini as comparison; Finally write a summary on K2.5's ability.", "file": "benchmark_thinking.png"}, | |
| ] | |
| def load_example(prompt, file_path=None): | |
| """Load an example prompt into the input.""" | |
| import os | |
| if file_path: | |
| # Return absolute path for the file - Gradio will handle the postprocessing | |
| return prompt, [os.path.abspath(file_path)] | |
| return prompt, None | |
| # Simple CSS | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@200;300;400;500&display=swap'); | |
| .gradio-container { | |
| font-family: 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif !important; | |
| font-weight: 200 !important; | |
| } | |
| /* Make input and button taller */ | |
| .gr-text-input textarea { | |
| padding: 16px !important; | |
| min-height: 56px !important; | |
| } | |
| .gr-button { | |
| padding: 12px 24px !important; | |
| min-height: 56px !important; | |
| } | |
| .header { | |
| text-align: center; | |
| padding: 20px; | |
| margin-bottom: 10px; | |
| } | |
| .header h1 { | |
| font-size: 32px; | |
| font-weight: 500; | |
| margin: 0; | |
| } | |
| .header .highlight { | |
| background: linear-gradient(135deg, #0f0b05, #054e0b); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .sidebar-info { | |
| padding: 15px; | |
| background: #f9fafb; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| color: #666; | |
| } | |
| .quick-load-title { | |
| margin-top: 16px !important; | |
| margin-bottom: 8px !important; | |
| color: #92400e !important; | |
| } | |
| .quick-load-title p { | |
| margin: 0 !important; | |
| } | |
| /* Quick load container styling - lower saturation background */ | |
| .quick-load-container { | |
| background: #f5f5f4 !important; | |
| border: 1px solid #e7e5e4 !important; | |
| border-radius: 8px !important; | |
| padding: 12px !important; | |
| } | |
| /* Quick load button styling */ | |
| .quick-load-btn { | |
| margin-bottom: 6px !important; | |
| font-size: 13px !important; | |
| } | |
| """ | |
| # Create Gradio interface | |
| with gr.Blocks(title="Chat with K2.5", css=custom_css, theme=gr.themes.Soft(primary_hue="blue", secondary_hue="blue", neutral_hue="blue")) as demo: | |
| # Header | |
| gr.HTML(""" | |
| <div class="header"> | |
| <h1 style="display: inline-flex; align-items: center; justify-content: center; gap: 12px; vertical-align: middle;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 66 26" fill="none" style="height: 58px; width: auto; flex-shrink: 0;"> | |
| <rect width="66" height="26" fill="#000" rx="13"></rect> | |
| <path fill="#fff" d="M27.65 19.706c0 .163.132.294.294.294h1.888a.294.294 0 0 0 .294-.294V6.294A.294.294 0 0 0 29.832 6h-1.888a.294.294 0 0 0-.294.294zM51.525 19.706c0 .163.131.294.293.294h1.888a.294.294 0 0 0 .294-.294V6.294A.294.294 0 0 0 53.706 6h-1.888a.294.294 0 0 0-.293.294zM44.058 6a.294.294 0 0 0-.285.224l-2.79 11.419c-.047.187-.267.187-.312 0L37.88 6.223A.29.29 0 0 0 37.596 6h-4.73a.29.29 0 0 0-.291.294v13.412c0 .163.13.294.293.294h2.097a.29.29 0 0 0 .293-.29V8.43c0-.226.265-.272.319-.056l2.787 11.402c.032.131.15.224.285.224h4.355a.294.294 0 0 0 .285-.224l2.786-11.4c.053-.217.318-.17.318.055v11.275c0 .163.132.294.294.294h2.096a.294.294 0 0 0 .294-.294V6.294A.294.294 0 0 0 48.783 6zM18.697 12.491l5.692-5.995A.294.294 0 0 0 24.176 6h-2.65a.3.3 0 0 0-.208.087l-6.524 6.573c-.101.102-.251.011-.251-.153V6.294A.294.294 0 0 0 14.249 6h-1.955a.294.294 0 0 0-.294.294v13.412c0 .163.132.294.294.294h1.955a.294.294 0 0 0 .294-.294v-2.674a.22.22 0 0 1 .057-.153l2.013-2.052c.049-.05.116-.057.17-.02l5.385 4.07c.788.546 1.79.902 2.709 1.052a.282.282 0 0 0 .325-.284v-2.423a.306.306 0 0 0-.246-.296c-.532-.114-1.125-.331-1.574-.642l-4.662-3.467c-.096-.067-.109-.238-.023-.326"></path> | |
| </svg> | |
| <span style="font-size: 1.8em;">Chat with <span class="highlight">K2.5</span></span> | |
| </h1> | |
| </div> | |
| """) | |
| # Main chat area (define first so msg_input exists) | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| chatbot = gr.Chatbot( | |
| label="Chat", | |
| height=700, | |
| avatar_images=("https://api.dicebear.com/7.x/initials/svg?seed=User&backgroundColor=e0f2fe&textColor=0ea5e9", "https://upload.wikimedia.org/wikipedia/en/8/87/Kimi-logo-2025.png") | |
| ) | |
| with gr.Row(): | |
| msg_input = gr.Textbox( | |
| placeholder="Type your message here...", | |
| show_label=False, | |
| scale=4 | |
| ) | |
| send_btn = gr.Button("Send", variant="primary", scale=1) | |
| file_input = gr.File( | |
| label="Upload Images/Videos (optional)", | |
| file_types=["image", "video"], | |
| file_count="multiple" | |
| ) | |
| # Left sidebar (define after so we can reference msg_input) | |
| with gr.Column(scale=1): | |
| new_chat_btn = gr.Button("✨ New Chat", variant="primary") | |
| thinking_btn = gr.Button("⚡ Thinking: ON", variant="secondary") | |
| gr.HTML(f""" | |
| <div class="sidebar-info"> | |
| Model: {MODEL}<br> | |
| API: {base_url} | |
| </div> | |
| <div style="margin-top: 16px; padding: 12px; background: linear-gradient(135deg, #e0f2fe 0%, #dbeafe 100%); border-radius: 8px; border-left: 3px solid #3b82f6; font-size: 14px; color: #1e40af;"> | |
| <strong>📖 How to Use</strong><br><br> | |
| • <strong>Chat:</strong> Type your message and press Enter or click Send<br> | |
| • <strong>Upload Image:</strong> Click "Upload Images/Videos" to add images; Support JPG, PNG, etc.<br> | |
| • <strong>Upload Video:</strong> Support MP4, AVI, MOV, etc.<br> | |
| • <strong>⚠️ Note:</strong> Video size should not exceed <strong>100MB</strong><br> | |
| • <strong>Thinking Mode:</strong> Toggle "Thinking" button to enable/disable reasoning | |
| </div> | |
| <div style="margin-top: 16px; padding: 12px; background: linear-gradient(135deg, #e0f2fe 0%, #dbeafe 100%); border-radius: 8px; border-left: 3px solid #3b82f6; font-size: 14px; color: #1e40af;"> | |
| <strong>✨ Hungry for More?</strong><br><br> | |
| <strong>💬 Chat Users:</strong> Head to <a href="https://kimi.com" target="_blank" style="color: #2563eb; text-decoration: underline; font-weight: bold;">kimi.com</a> for the full experience—complete with agentic magic that'll make your workflow dance.<br><br> | |
| <strong>🚀 Pro Users:</strong> For API enthusiasts, fuel your creations at <a href="https://platform.moonshot.ai" target="_blank" style="color: #2563eb; text-decoration: underline; font-weight: bold;">platform.moonshot.ai</a>. Your wallet will thank you, and your projects will too. | |
| </div> | |
| """) | |
| # Quick Load Examples | |
| gr.Markdown(f"""<div style="margin-top: 16px; padding: 12px; background: linear-gradient(135deg, #d1d5db 0%, #d1d5db 100%); border-radius: 8px; border-left: 3px solid #6b7280; font-size: 14px; color: #374151;"> | |
| <strong>⚡ Quick Load Examples</strong> | |
| </div>""") | |
| with gr.Column(variant="panel"): | |
| quick_load_buttons = [] | |
| for example in QUICK_EXAMPLES: | |
| btn = gr.Button( | |
| example["label"], | |
| size="sm", | |
| variant="secondary", | |
| elem_classes="quick-load-btn" | |
| ) | |
| quick_load_buttons.append((btn, example["prompt"], example.get("file"))) | |
| # Event handlers | |
| new_chat_btn.click( | |
| create_new_conversation, | |
| outputs=[chatbot, file_input, thinking_btn] | |
| ) | |
| thinking_btn.click( | |
| toggle_thinking, | |
| outputs=[thinking_btn] | |
| ) | |
| # Quick Load button handlers | |
| for btn, prompt, file_path in quick_load_buttons: | |
| btn.click( | |
| fn=lambda p=prompt, f=file_path: load_example(p, f), | |
| outputs=[msg_input, file_input] | |
| ) | |
| # Send message | |
| send_btn.click( | |
| user_message, | |
| inputs=[msg_input, file_input], | |
| outputs=[chatbot, msg_input, file_input] | |
| ).then( | |
| bot_response, | |
| outputs=[chatbot] | |
| ) | |
| # Enter key to send | |
| msg_input.submit( | |
| user_message, | |
| inputs=[msg_input, file_input], | |
| outputs=[chatbot, msg_input, file_input] | |
| ).then( | |
| bot_response, | |
| outputs=[chatbot] | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False | |
| ) | |