Vibe-Game / app.py
openfree's picture
Create app.py
dc94023 verified
raw
history blame
30.1 kB
import os
import re
import random
import time
import html
import base64
import string
import json
import asyncio
import requests
import anthropic
import openai
from http import HTTPStatus
from typing import Dict, List, Optional, Tuple
from functools import partial
import gradio as gr
import modelscope_studio.components.base as ms
import modelscope_studio.components.legacy as legacy
import modelscope_studio.components.antd as antd
class Role:
SYSTEM = "system"
USER = "user"
ASSISTANT = "assistant"
DEMO_LIST = [
{"description": "블록이 위에서 떨어지는 클래식 테트리스 게임을 개발해주세요..."},
{"description": "두 명이 번갈아가며 플레이할 수 있는 체스 게임을 만들어주세요..."},
# ... 중략 (게임 템플릿) ...
]
SystemPrompt = """# GameCraft 시스템 프롬프트
(중략)
"""
History = List[Tuple[str, str]]
Messages = List[Dict[str, str]]
IMAGE_CACHE = {}
def get_image_base64(image_path):
if image_path in IMAGE_CACHE:
return IMAGE_CACHE[image_path]
try:
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode()
IMAGE_CACHE[image_path] = encoded_string
return encoded_string
except:
return IMAGE_CACHE.get('default.png', '')
def history_to_messages(history: History, system: str) -> Messages:
messages = [{'role': Role.SYSTEM, 'content': system}]
for h in history:
messages.append({'role': Role.USER, 'content': h[0]})
messages.append({'role': Role.ASSISTANT, 'content': h[1]})
return messages
def messages_to_history(messages: Messages) -> History:
assert messages[0]['role'] == Role.SYSTEM
history = []
for q, r in zip(messages[1::2], messages[2::2]):
history.append([q['content'], r['content']])
return history
YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip()
YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip()
claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN)
openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN)
async def try_claude_api(system_message, claude_messages, timeout=15):
try:
system_message_with_limit = system_message + "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 말 것..."
start_time = time.time()
with claude_client.messages.stream(
model="claude-2",
max_tokens=19800,
system=system_message_with_limit,
messages=claude_messages,
temperature=0.3,
) as stream:
collected_content = ""
for chunk in stream:
current_time = time.time()
if current_time - start_time > timeout:
raise TimeoutError("Claude API timeout")
if chunk.type == "content_block_delta":
collected_content += chunk.delta.text
yield collected_content
await asyncio.sleep(0)
start_time = current_time
except Exception as e:
raise e
async def try_openai_api(openai_messages):
try:
if openai_messages and openai_messages[0]["role"] == "system":
openai_messages[0]["content"] += "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 말 것..."
stream = openai_client.chat.completions.create(
model="gpt-4",
messages=openai_messages,
stream=True,
max_tokens=19800,
temperature=0.2
)
collected_content = ""
for chunk in stream:
if chunk.choices[0].delta.content is not None:
collected_content += chunk.choices[0].delta.content
yield collected_content
except Exception as e:
raise e
def load_json_data():
data_list = []
for item in DEMO_LIST:
data_list.append({
"name": f"[게임] {item['description'][:20]}...",
"prompt": item['description']
})
return data_list
def create_template_html(title, items):
import html as html_lib
html_content = r"""
<style>
.prompt-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
padding: 12px;
}
.prompt-card {
background: white;
border: 1px solid #eee;
border-radius: 12px;
padding: 12px;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
transition: all 0.3s ease;
}
.prompt-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
}
.card-name {
font-weight: bold;
margin-bottom: 8px;
font-size: 13px;
color: #444;
}
.card-prompt {
font-size: 11px;
line-height: 1.4;
color: #666;
display: -webkit-box;
-webkit-line-clamp: 7;
-webkit-box-orient: vertical;
overflow: hidden;
height: 84px;
background-color: #f8f9fa;
padding: 8px;
border-radius: 6px;
}
</style>
<div class="prompt-grid">
"""
for item in items:
card_html = f"""
<div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html_lib.escape(item.get('prompt', ''))}">
<div class="card-name">{html_lib.escape(item.get('name', ''))}</div>
<div class="card-prompt">{html_lib.escape(item.get('prompt', ''))}</div>
</div>
"""
html_content += card_html
html_content += r"""
</div>
<script>
function copyToInput(card) {
const prompt = card.dataset.prompt;
const textarea = document.querySelector('.ant-input-textarea-large textarea');
if (textarea) {
textarea.value = prompt;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
// 템플릿 Drawer 닫기
document.querySelector('.session-drawer .close-btn').click();
}
}
</script>
"""
return gr.HTML(value=html_content)
def load_all_templates():
return create_template_html("🎮 모든 게임 템플릿", load_json_data())
def remove_code_block(text):
pattern = r'```html\s*([\s\S]+?)\s*```'
match = re.search(pattern, text, re.DOTALL)
if match:
return match.group(1).strip()
pattern = r'```(?:\w+)?\s*([\s\S]+?)\s*```'
match = re.search(pattern, text, re.DOTALL)
if match:
return match.group(1).strip()
text = re.sub(r'```html\s*', '', text)
text = re.sub(r'\s*```', '', text)
return text.strip()
def optimize_code(code: str) -> str:
if not code or len(code.strip()) == 0:
return code
lines = code.split('\n')
if len(lines) <= 200:
return code
comment_patterns = [
r'/\*[\s\S]*?\*/',
r'//.*?$',
r'<!--[\s\S]*?-->'
]
cleaned_code = code
for pattern in comment_patterns:
cleaned_code = re.sub(pattern, '', cleaned_code, flags=re.MULTILINE)
cleaned_lines = []
empty_line_count = 0
for line in cleaned_code.split('\n'):
if line.strip() == '':
empty_line_count += 1
if empty_line_count <= 1:
cleaned_lines.append('')
else:
empty_line_count = 0
cleaned_lines.append(line)
cleaned_code = '\n'.join(cleaned_lines)
cleaned_code = re.sub(r'console\.log\(.*?\);', '', cleaned_code, flags=re.MULTILINE)
cleaned_code = re.sub(r' {2,}', ' ', cleaned_code)
return cleaned_code
def send_to_sandbox(code):
clean_code = remove_code_block(code)
clean_code = optimize_code(clean_code)
if clean_code.startswith('```html'):
clean_code = clean_code[7:].strip()
if clean_code.endswith('```'):
clean_code = clean_code[:-3].strip()
if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'):
clean_code = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Game Preview</title>
</head>
<body>
{clean_code}
</body>
</html>"""
encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8')
data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
return f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;"></iframe>'
def boost_prompt(prompt: str) -> str:
if not prompt:
return ""
boost_system_prompt = """당신은 웹 게임 개발 프롬프트 전문가입니다.
(중략)
"""
try:
# 시도1: Claude
try:
response = claude_client.messages.create(
model="claude-2",
max_tokens=10000,
temperature=0.3,
messages=[{
"role": "user",
"content": f"다음 게임 프롬프트를 분석하고 증강하되, 간결함을 유지하세요: {prompt}"
}],
system=boost_system_prompt
)
if hasattr(response, 'content') and len(response.content) > 0:
return response.content[0].text
raise Exception("Claude 응답 형식 오류")
except Exception:
# 시도2: OpenAI
completion = openai_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": boost_system_prompt},
{"role": "user", "content": f"다음 게임 프롬프트를 증강: {prompt}"}
],
max_tokens=10000,
temperature=0.3
)
if completion.choices and len(completion.choices) > 0:
return completion.choices[0].message.content
raise Exception("OpenAI 응답 형식 오류")
except Exception:
return prompt
def handle_boost(prompt: str):
try:
boosted_prompt = boost_prompt(prompt)
return boosted_prompt, gr.update(active_key="empty")
except:
return prompt, gr.update(active_key="empty")
def history_render(history: History):
return gr.update(open=True), history
def execute_code(query: str):
if not query or query.strip() == '':
return None, gr.update(active_key="empty")
try:
clean_code = remove_code_block(query)
if clean_code.startswith('```html'):
clean_code = clean_code[7:].strip()
if clean_code.endswith('```'):
clean_code = clean_code[:-3].strip()
if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'):
if not ('<body' in clean_code and '</body>' in clean_code):
clean_code = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Game Preview</title>
</head>
<body>
{clean_code}
</body>
</html>"""
return send_to_sandbox(clean_code), gr.update(active_key="render")
except Exception as e:
print(f"Execute code error: {str(e)}")
return None, gr.update(active_key="empty")
class Demo:
def __init__(self):
pass
async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
if not query or query.strip() == '':
query = random.choice(DEMO_LIST)['description']
if _history is None:
_history = []
query = f"""
다음 게임을 제작해주세요.
(중략)
게임 요청: {query}
"""
messages = history_to_messages(_history, _setting['system'])
system_message = messages[0]['content']
claude_messages = [
{"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]}
for msg in messages[1:] + [{'role': Role.USER, 'content': query}]
if msg["content"].strip() != ''
]
openai_messages = [{"role": "system", "content": system_message}]
for msg in messages[1:]:
openai_messages.append({
"role": msg["role"],
"content": msg["content"]
})
openai_messages.append({"role": "user", "content": query})
try:
yield [
"Generating code...",
_history,
None,
gr.update(active_key="loading"),
gr.update(open=True)
]
await asyncio.sleep(0)
collected_content = None
try:
async for content in try_claude_api(system_message, claude_messages):
yield [
content,
_history,
None,
gr.update(active_key="loading"),
gr.update(open=True)
]
await asyncio.sleep(0)
collected_content = content
except Exception:
async for content in try_openai_api(openai_messages):
yield [
content,
_history,
None,
gr.update(active_key="loading"),
gr.update(open=True)
]
await asyncio.sleep(0)
collected_content = content
if collected_content:
clean_code = remove_code_block(collected_content)
code_lines = clean_code.count('\n') + 1
if code_lines > 700:
warning_msg = f"""
⚠️ **경고: 생성된 코드가 너무 깁니다 ({code_lines}줄)**
(중략)
"""
collected_content = warning_msg
yield [
collected_content,
_history,
None,
gr.update(active_key="empty"),
gr.update(open=True)
]
else:
_history = messages_to_history([
{'role': Role.SYSTEM, 'content': system_message}
] + claude_messages + [{
'role': Role.ASSISTANT,
'content': collected_content
}])
yield [
collected_content,
_history,
send_to_sandbox(clean_code),
gr.update(active_key="render"),
gr.update(open=True)
]
else:
raise ValueError("No content generated.")
except Exception as e:
raise ValueError(f'Error calling APIs: {str(e)}')
def clear_history(self):
return []
demo_instance = Demo()
theme = gr.themes.Soft(
primary_hue="blue",
secondary_hue="purple",
neutral_hue="slate",
spacing_size=gr.themes.sizes.spacing_md,
radius_size=gr.themes.sizes.radius_md,
text_size=gr.themes.sizes.text_md,
)
with gr.Blocks(css_paths=["app.css"], theme=theme) as demo:
header_html = gr.HTML("""
<div class="app-header">
<h1>🎮 Vibe Game Craft</h1>
<p>설명을 입력하면 웹 기반 HTML5, JavaScript, CSS 게임을 생성합니다...</p>
</div>
<!-- 배포 결과 배너 - 헤더 바로 아래 위치 -->
<div id="deploy-banner" style="display:none;" class="deploy-banner">
<div class="deploy-banner-content">
<div class="deploy-banner-icon">🚀</div>
<div class="deploy-banner-info">
<div id="deploy-banner-title" class="deploy-banner-title">배포 상태</div>
<div id="deploy-banner-message" class="deploy-banner-message"></div>
</div>
<div id="deploy-banner-url-container" class="deploy-banner-url-container" style="display:none;">
<a id="deploy-banner-url" href="#" target="_blank" class="deploy-banner-url"></a>
<button onclick="copyBannerUrl()" class="deploy-banner-copy-btn">복사</button>
</div>
</div>
</div>
<script>
function copyBannerUrl() {
const url = document.getElementById('deploy-banner-url').href;
navigator.clipboard.writeText(url).then(()=>{
const copyBtn = document.querySelector('.deploy-banner-copy-btn');
const originalText = copyBtn.textContent;
copyBtn.textContent = '복사됨!';
setTimeout(()=>{copyBtn.textContent=originalText;},1000);
});
}
function showDeployBanner(type, title, message, url) {
const banner = document.getElementById('deploy-banner');
banner.className = 'deploy-banner '+type;
document.getElementById('deploy-banner-title').textContent = title;
document.getElementById('deploy-banner-message').textContent = message;
const urlContainer = document.getElementById('deploy-banner-url-container');
const bannerUrl = document.getElementById('deploy-banner-url');
if(url){
bannerUrl.href=url; bannerUrl.textContent=url;
urlContainer.style.display='flex';
} else { urlContainer.style.display='none'; }
banner.style.display='block';
}
</script>
""")
history = gr.State([])
setting = gr.State({"system": SystemPrompt})
deploy_status = gr.State({"is_deployed":False,"status":"","url":"","message":""})
with ms.Application() as app:
with antd.ConfigProvider():
with antd.Drawer(open=False, title="코드 보기", placement="left", width="750px") as code_drawer:
code_output = legacy.Markdown()
with antd.Drawer(open=False, title="히스토리", placement="left", width="900px") as history_drawer:
history_output = legacy.Chatbot(show_label=False, flushing=False, height=960, elem_classes="history_chatbot")
with antd.Drawer(open=False, title="게임 템플릿", placement="right", width="900px", elem_classes="session-drawer") as session_drawer:
with antd.Flex(vertical=True, gap="middle"):
gr.Markdown("### 사용 가능한 게임 템플릿")
session_history = gr.HTML(elem_classes="session-history")
close_btn = antd.Button("닫기", type="default", elem_classes="close-btn")
with antd.Row(gutter=[32,12], align="top", elem_classes="equal-height-container") as layout:
with antd.Col(span=24, md=16, elem_classes="equal-height-col"):
with ms.Div(elem_classes="right_panel panel"):
gr.HTML("""
<div class="render_header">
<span class="header_btn"></span>
<span class="header_btn"></span>
<span class="header_btn"></span>
</div>
""")
with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
with antd.Tabs.Item(key="empty"):
empty = antd.Empty(description="게임을 만들려면 설명을 입력하세요", elem_classes="right_content")
with antd.Tabs.Item(key="loading"):
loading = antd.Spin(True, tip="게임 코드 생성 중...", size="large", elem_classes="right_content")
with antd.Tabs.Item(key="render"):
sandbox = gr.HTML(elem_classes="html_content")
with antd.Col(span=24, md=8, elem_classes="equal-height-col"):
with antd.Flex(vertical=True, gap="small", elem_classes="right-top-buttons"):
with antd.Flex(gap="small", elem_classes="setting-buttons", justify="space-between"):
codeBtn = antd.Button("🧑‍💻 코드 보기", type="default", elem_classes="code-btn")
historyBtn = antd.Button("📜 히스토리", type="default", elem_classes="history-btn")
template_btn = antd.Button("🎮 템플릿", type="default", elem_classes="template-btn")
with antd.Flex(gap="small", justify="space-between", elem_classes="action-buttons"):
btn = antd.Button("전송", type="primary", size="large", elem_classes="send-btn")
boost_btn = antd.Button("증강", type="default", size="large", elem_classes="boost-btn")
execute_btn = antd.Button("코드", type="default", size="large", elem_classes="execute-btn")
deploy_btn = antd.Button("배포", type="default", size="large", elem_classes="deploy-btn")
clear_btn = antd.Button("클리어", type="default", size="large", elem_classes="clear-btn")
with antd.Flex(vertical=True, gap="middle", wrap=True, elem_classes="input-panel"):
input_text = antd.InputTextarea(
size="large",
allow_clear=True,
placeholder="예) 테트리스 게임 만들기...",
max_length=100000
)
gr.HTML('<div class="help-text">💡 원하는 게임의 설명을 입력하세요.</div>')
# 별도의 배포 결과 섹션(박스)
deploy_result_container = gr.HTML("""
<div class="deploy-section">
<div class="deploy-header">📤 배포 결과</div>
<div id="deploy-result-box" class="deploy-result-box">
<div class="no-deploy">아직 배포된 게임이 없습니다.</div>
</div>
</div>
""")
js_trigger = gr.HTML(elem_id="js-trigger", visible=False)
# Drawer 이벤트
codeBtn.click(lambda: gr.update(open=True), inputs=[], outputs=[code_drawer])
code_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[code_drawer])
historyBtn.click(history_render, inputs=[history], outputs=[history_drawer, history_output])
history_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[history_drawer])
template_btn.click(fn=lambda: (gr.update(open=True), load_all_templates()),
outputs=[session_drawer, session_history], queue=False)
session_drawer.close(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history])
close_btn.click(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history])
btn.click(
demo_instance.generation_code,
inputs=[input_text, setting, history],
outputs=[code_output, history, sandbox, state_tab, code_drawer]
)
clear_btn.click(demo_instance.clear_history, inputs=[], outputs=[history])
boost_btn.click(handle_boost, inputs=[input_text], outputs=[input_text, state_tab])
execute_btn.click(execute_code, inputs=[input_text], outputs=[sandbox, state_tab])
def deploy_to_vercel(code: str):
try:
token = "YOUR_VERCEL_TOKEN" # 실제 토큰
if not token:
return {"status": "error", "message": "Vercel 토큰이 설정되지 않았습니다."}
project_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(6))
deploy_url = "https://api.vercel.com/v13/deployments"
headers = {"Authorization": f"Bearer {token}","Content-Type": "application/json"}
package_json = {
"name": project_name,
"version": "1.0.0",
"private": True,
"dependencies": {"vite": "^5.0.0"},
"scripts": {
"dev": "vite",
"build": "echo 'No build needed' && mkdir -p dist && cp index.html dist/",
"preview": "vite preview"
}
}
files = [
{"file": "index.html", "data": code},
{"file": "package.json", "data": json.dumps(package_json, indent=2)}
]
project_settings = {
"buildCommand": "npm run build",
"outputDirectory": "dist",
"installCommand": "npm install",
"framework": None
}
deploy_data = {
"name": project_name,
"files": files,
"target": "production",
"projectSettings": project_settings
}
r = requests.post(deploy_url, headers=headers, json=deploy_data)
if r.status_code != 200:
return {"status":"error","message":f"배포 실패: {r.text}"}
deployment_url = f"https://{project_name}.vercel.app"
time.sleep(5) # 실제 배포가 되기를 잠시 대기
return {"status":"success","url":deployment_url,"project_name":project_name}
except Exception as e:
return {"status":"error","message":f"배포 중 오류: {e}"}
def handle_deploy(code, deploy_status):
if not code:
js_code = """
<script>
showDeployBanner('error','⚠️ 배포 실패','배포할 코드가 없습니다. 먼저 게임 코드를 생성해주세요.','');
document.getElementById('deploy-result-box').innerHTML = `
<div class="deploy-error">
<div class="error-icon">⚠️</div>
<div class="error-message">배포할 코드가 없습니다.</div>
</div>
`;
</script>
"""
return js_code, {"is_deployed":False,"status":"error","message":"코드 없음","url":""}
try:
loading_js = """
<script>
showDeployBanner('loading','🔄 배포 진행 중','Vercel에 게임을 배포하고 있습니다... 잠시 기다려주세요.', '');
document.getElementById('deploy-result-box').innerHTML = `
<div class="deploy-loading">
<div class="loading-spinner"></div>
<div class="loading-message">Vercel에 배포 중...</div>
</div>
`;
</script>
"""
yield loading_js, deploy_status
clean_code = remove_code_block(code)
result = deploy_to_vercel(clean_code)
if result.get("status")=="success":
url = result["url"]
success_js = f"""
<script>
showDeployBanner('success','✅ 배포 완료','게임이 성공적으로 배포되었습니다.','{url}');
document.getElementById('deploy-result-box').innerHTML = `
<div class="deploy-success">
<div class="success-icon">✅</div>
<div>배포 성공!</div>
<div><a href="{url}" target="_blank">{url}</a></div>
</div>
`;
</script>
"""
return success_js, {
"is_deployed": True,
"status":"success","url":url,"message":"배포 완료"
}
else:
msg = result.get("message","알 수 없는 오류")
error_js = f"""
<script>
showDeployBanner('error','⚠️ 배포 실패','{msg}','');
document.getElementById('deploy-result-box').innerHTML=`
<div class="deploy-error">
<div class="error-icon">⚠️</div>
<div class="error-message">배포 실패: {msg}</div>
</div>
`;
</script>
"""
return error_js, {"is_deployed":False,"status":"error","url":"","message":msg}
except Exception as e:
err_msg = str(e)
exception_js = f"""
<script>
showDeployBanner('error','⚠️ 시스템 오류','{err_msg}','');
document.getElementById('deploy-result-box').innerHTML=`
<div class="deploy-error">
<div class="error-icon">⚠️</div>
<div class="error-message">시스템 오류: {err_msg}</div>
</div>
`;
</script>
"""
return exception_js, {"is_deployed":False,"status":"error","url":"","message":err_msg}
deploy_btn.click(
fn=handle_deploy,
inputs=[code_output, deploy_status],
outputs=[deploy_result_container, deploy_status]
)
if __name__ == "__main__":
try:
demo_instance = Demo()
demo.queue(default_concurrency_limit=20).launch(ssr_mode=False)
except Exception as e:
print(f"Initialization error: {e}")
raise