Spaces:
Running
Running
| # ──────────────────────────────── Imports ──────────────────────────────── | |
| import os, json, re, logging, requests, markdown, time, io | |
| from datetime import datetime | |
| import streamlit as st | |
| from openai import OpenAI # OpenAI 라이브러리 | |
| from gradio_client import Client | |
| import pandas as pd | |
| import PyPDF2 # For handling PDF files | |
| # ──────────────────────────────── Environment Variables / Constants ───────────────────────── | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") | |
| BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Keep this name | |
| BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search" | |
| IMAGE_API_URL = "http://211.233.58.201:7896" # 이미지 생성용 API | |
| MAX_TOKENS = 7999 | |
| # ──────────────────────────────── Physical Transformation Categories (KR & EN) ───────────────── | |
| physical_transformation_categories = { | |
| "센서 기능": [ | |
| "시각 센서/감지", "청각 센서/감지", "촉각 센서/감지", "미각 센서/감지", "후각 센서/감지", | |
| "온도 센서/감지", "습도 센서/감지", "압력 센서/감지", "가속도 센서/감지", "회전 센서/감지", | |
| "근접 센서/감지", "위치 센서/감지", "운동 센서/감지", "가스 센서/감지", "적외선 센서/감지", | |
| "자외선 센서/감지", "방사선 센서/감지", "자기장 센서/감지", "전기장 센서/감지", "화학물질 센서/감지", | |
| "생체신호 센서/감지", "진동 센서/감지", "소음 센서/감지", "빛 세기 센서/감지", "빛 파장 센서/감지", | |
| "기울기 센서/감지", "pH 센서/감지", "전류 센서/감지", "전압 센서/감지", "이미지 센서/감지", | |
| "거리 센서/감지", "깊이 센서/감지", "중력 센서/감지", "속도 센서/감지", "흐름 센서/감지", | |
| "수위 센서/감지", "탁도 센서/감지", "염도 센서/감지", "금속 감지", "압전 센서/감지", | |
| "광전 센서/감지", "열전대 센서/감지", "홀 효과 센서/감지", "초음파 센서/감지", "레이더 센서/감지", | |
| "라이다 센서/감지", "터치 센서/감지", "제스처 센서/감지", "심박 센서/감지", "혈압 센서/감지" | |
| ], | |
| "크기와 형태 변화": [ | |
| "부피 늘어남/줄어듦", "길이 늘어남/줄어듦", "너비 늘어남/줄어듦", "높이 늘어남/줄어듦", | |
| "밀도 변화", "무게 증가/감소", "모양 변형", "상태 변화", "불균등 변형", | |
| "복잡한 형태 변형", "비틀림/꼬임", "불균일한 확장/축소", "모서리 둥글게/날카롭게", | |
| "깨짐/갈라짐", "여러 조각 나눠짐", "물 저항", "먼지 저항", "찌그러짐/복원", | |
| "접힘/펼쳐짐", "압착/팽창", "늘어남/수축", "구겨짐/평평해짐", "뭉개짐/단단해짐", | |
| "말림/펴짐", "꺾임/구부러짐" | |
| ], | |
| "표면 및 외관 변화": [ | |
| "색상 변화", "질감 변화", "투명/불투명 변화", "반짝임/무광 변화", | |
| "빛 반사 정도 변화", "무늬 변화", "각도에 따른 색상 변화", "빛에 따른 색상 변화", | |
| "온도에 따른 색상 변화", "홀로그램 효과", "표면 각도별 빛 반사", "표면 모양 변형", | |
| "초미세 표면 구조 변화", "자가 세정 효과", "얼룩/패턴 생성", "흐림/선명함 변화", | |
| "광택/윤기 변화", "색조/채도 변화", "발광/형광", "빛 산란 효과", | |
| "빛 흡수 변화", "반투명 효과", "그림자 효과 변화", "자외선 반응 변화", | |
| "야광 효과" | |
| ], | |
| "물질의 상태 변화": [ | |
| "고체/액체/기체 전환", "결정화/용해", "산화/부식", "딱딱해짐/부드러워짐", | |
| "특수 상태 전환", "무정형/결정형 전환", "성분 분리", "미세 입자 형성/분해", | |
| "젤 형성/풀어짐", "준안정 상태 변화", "분자 자가 정렬/분해", "상태변화 지연 현상", | |
| "녹음", "굳음", "증발/응축", "승화/증착", "침전/부유", "분산/응집", | |
| "건조/습윤", "팽윤/수축", "동결/해동", "풍화/침식", "충전/방전", | |
| "결합/분리", "발효/부패" | |
| ], | |
| "움직임 특성 변화": [ | |
| "가속/감속", "일정 속도 유지", "진동/진동 감소", "부딪힘/튕김", | |
| "회전 속도 증가/감소", "회전 방향 변화", "불규칙 움직임", "멈췄다 미끄러지는 현상", | |
| "공진/반공진", "유체 속 저항/양력 변화", "움직임 저항 변화", "복합 진동 움직임", | |
| "특수 유체 속 움직임", "회전-이동 연계 움직임", "관성 정지", "충격 흡수", | |
| "충격 전달", "운동량 보존", "마찰력 변화", "관성 탈출", "불안정 균형", | |
| "동적 안정성", "흔들림 감쇠", "경로 예측성", "회피 움직임" | |
| ], | |
| "구조적 변화": [ | |
| "부품 추가/제거", "조립/분해", "접기/펴기", "변형/원상복구", "최적 구조 변화", | |
| "자가 재배열", "자연 패턴 형성/소멸", "규칙적 패턴 변화", "모듈식 변형", | |
| "복잡성 증가 구조", "원래 모양 기억 효과", "시간에 따른 형태 변화", "부분 제거", | |
| "부분 교체", "결합", "분리", "분할/통합", "중첩/겹침", "내부 구조 변화", | |
| "외부 구조 변화", "중심축 이동", "균형점 변화", "계층 구조 변화", "지지 구조 변화", | |
| "응력 분산 구조", "충격 흡수 구조", "그리드/매트릭스 구조 변화", "상호 연결성 변화" | |
| ], | |
| "공간 이동": [ | |
| "앞/뒤 이동", "좌/우 이동", "위/아래 이동", "세로축 회전(고개 끄덕임)", | |
| "가로축 회전(고개 젓기)", "길이축 회전(옆으로 기울임)", "원 운동", "나선형 이동", | |
| "관성에 의한 미끄러짐", "회전축 변화", "불규칙 회전", "흔들림 운동", "포물선 이동", | |
| "무중력 부유", "수면 위 부유", "점프/도약", "슬라이딩", "롤링", "자유 낙하", | |
| "왕복 운동", "탄성 튕김", "관통", "회피 움직임", "지그재그 이동", "스윙 운동" | |
| ], | |
| "시간 관련 변화": [ | |
| "노화/풍화", "마모/부식", "색 바램/변색", "손상/회복", "수명 주기 변화", | |
| "사용자 상호작용에 따른 적응", "학습 기반 형태 최적화", "시간에 따른 물성 변화", | |
| "집단 기억 효과", "문화적 의미 변화", "지연 반응", "이전 상태 의존 변화", | |
| "점진적 시간 변화", "진화적 변화", "주기적 재생", "계절 변화 적응", | |
| "생체리듬 변화", "생애 주기 단계", "성장/퇴화", "자가 복구/재생", | |
| "자연 순환 적응", "지속성/일시성", "기억 효과", "지연된 작용", "누적 효과" | |
| ], | |
| "빛과 시각 효과": [ | |
| "발광/소등", "빛 투과/차단", "빛 산란/집중", "색상 스펙트럼 변화", "빛 회절", | |
| "빛 간섭", "홀로그램 생성", "레이저 효과", "빛 편광", "형광/인광", | |
| "자외선/적외선 발광", "광학적 착시", "빛 굴절", "그림자 생성/제거", | |
| "색수차 효과", "무지개 효과", "글로우 효과", "플래시 효과", "조명 패턴", | |
| "빔 효과", "광 필터 효과", "빛의 방향성 변화", "투영 효과", "빛 감지/반응", | |
| "광도 변화" | |
| ], | |
| "소리와 진동 효과": [ | |
| "소리 발생/소멸", "음 높낮이 변화", "음량 변화", "음색 변화", | |
| "공명/반공명", "음향 진동", "초음파/저음파 발생", "소리 집중/분산", | |
| "음향 반사/흡수", "음향 도플러 효과", "음파 간섭", "음향 공진", | |
| "진동 패턴 변화", "타악 효과", "음향 피드백", "음향 차폐/증폭", | |
| "소리 지향성", "소리 왜곡", "비트 생성", "배음 생성", "주파수 변조", | |
| "음향 충격파", "음향 필터링" | |
| ], | |
| "열 관련 변화": [ | |
| "온도 상승/하강", "열 팽창/수축", "열 전달/차단", "압력 상승/하강", | |
| "열 변화에 따른 자화", "엔트로피 변화", "열전기 효과", "자기장에 의한 열 변화", | |
| "상태 변화 중 열 저장/방출", "열 스트레스 발생/해소", "급격한 온도 변화 영향", | |
| "복사 냉각/가열", "발열/흡열", "열 분포 변화", "열 반사/흡수", | |
| "냉각 응축", "열 활성화", "열 변색", "열 팽창 계수 변화", "열 안정성 변화", | |
| "내열성/내한성", "자가 발열", "열적 평형/불균형", "열적 변형", "열 분산/집중" | |
| ], | |
| "전기 및 자기 변화": [ | |
| "자성 생성/소멸", "전하량 증가/감소", "전기장 생성/소멸", "자기장 생성/소멸", | |
| "초전도 상태 전환", "강유전체 특성 변화", "양자 상태 변화", "플라즈마 형성/소멸", | |
| "스핀파 전달", "빛에 의한 전기 발생", "압력에 의한 전기 발생", "자기장 내 전류 변화", | |
| "전기 저항 변화", "전기 전도성 변화", "정전기 발생/방전", "전자기 유도", | |
| "전자기파 방출/흡수", "전기 용량 변화", "자기 이력 현상", "전기적 분극", | |
| "전자 흐름 방향 변화", "전기적 공명", "전기적 차폐/노출", "자기 차폐/노출", | |
| "자기장 정렬" | |
| ], | |
| "화학적 변화": [ | |
| "표면 코팅 변화", "물질 성분 변화", "화학 반응 변화", "촉매 작용 시작/중단", | |
| "빛에 의한 화학 반응", "전기에 의한 화학 반응", "단분자막 형성", "분자 수준 구조 변화", | |
| "생체 모방 표면 변화", "환경 반응형 물질 변화", "주기적 화학 반응", "산화", "환원", | |
| "고분자화", "물 분해", "화합", "방사선 영향", "산-염기 반응", "중화 반응", | |
| "이온화", "화학적 흡착/탈착", "촉매 효율 변화", "효소 활성 변화", "발색 반응", | |
| "pH 변화", "화학적 평형 이동", "결합 형성/분해", "용해도 변화" | |
| ], | |
| "생물학적 변화": [ | |
| "성장/위축", "세포 분열/사멸", "생물 발광", "신진대사 변화", "면역 반응", | |
| "호르몬 분비", "신경 반응", "유전적 발현", "적응/진화", "생체리듬 변화", | |
| "재생/치유", "노화/성숙", "생체 모방 변화", "바이오필름 형성", "생물학적 분해", | |
| "효소 활성화/비활성화", "생물학적 신호 전달", "스트레스 반응", "체온 조절", "생물학적 시계 변화", | |
| "세포외 기질 변화", "생체 역학적 반응", "세포 운동성", "세포 극성 변화", "영양 상태 변화" | |
| ], | |
| "환경 상호작용": [ | |
| "온도 반응", "습도 반응", "기압 반응", "중력 반응", "자기장 반응", | |
| "빛 반응", "소리 반응", "화학 물질 감지", "기계적 자극 감지", "전기 자극 반응", | |
| "방사선 반응", "진동 감지", "pH 반응", "용매 반응", "기체 교환", | |
| "환경 오염 반응", "날씨 반응", "계절 반응", "일주기 반응", "생태계 상호작용", | |
| "공생/경쟁 반응", "포식/피식 관계", "군집 형성", "영역 설정", "이주/정착 패턴" | |
| ], | |
| "비즈니스 아이디어": [ | |
| "시장 재정의/신규 시장 개척", | |
| "비즈니스 모델 혁신/디지털 전환", | |
| "고객 경험 혁신/서비스 혁신", | |
| "협력 및 파트너십 강화/생태계 구축", | |
| "글로벌 확장/지역화 전략", | |
| "운영 효율성 증대/원가 절감", | |
| "브랜드 리포지셔닝/이미지 전환", | |
| "지속 가능한 성장/사회적 가치 창출", | |
| "데이터 기반 의사결정/AI 도입", | |
| "신기술 융합/혁신 투자" | |
| ] | |
| } | |
| physical_transformation_categories_en = { | |
| "Sensor Functions": [ | |
| "Visual sensor/detection", "Auditory sensor/detection", "Tactile sensor/detection", "Taste sensor/detection", "Olfactory sensor/detection", | |
| "Temperature sensor/detection", "Humidity sensor/detection", "Pressure sensor/detection", "Acceleration sensor/detection", "Rotational sensor/detection", | |
| "Proximity sensor/detection", "Position sensor/detection", "Motion sensor/detection", "Gas sensor/detection", "Infrared sensor/detection", | |
| "Ultraviolet sensor/detection", "Radiation sensor/detection", "Magnetic sensor/detection", "Electric field sensor/detection", "Chemical sensor/detection", | |
| "Biosignal sensor/detection", "Vibration sensor/detection", "Noise sensor/detection", "Light intensity sensor/detection", "Light wavelength sensor/detection", | |
| "Tilt sensor/detection", "pH sensor/detection", "Current sensor/detection", "Voltage sensor/detection", "Image sensor/detection", | |
| "Distance sensor/detection", "Depth sensor/detection", "Gravity sensor/detection", "Speed sensor/detection", "Flow sensor/detection", | |
| "Water level sensor/detection", "Turbidity sensor/detection", "Salinity sensor/detection", "Metal detection", "Piezoelectric sensor/detection", | |
| "Photovoltaic sensor/detection", "Thermocouple sensor/detection", "Hall effect sensor/detection", "Ultrasonic sensor/detection", "Radar sensor/detection", | |
| "Lidar sensor/detection", "Touch sensor/detection", "Gesture sensor/detection", "Heart rate sensor/detection", "Blood pressure sensor/detection" | |
| ], | |
| "Size and Shape Change": [ | |
| "Volume increase/decrease", "Length increase/decrease", "Width increase/decrease", "Height increase/decrease", | |
| "Density change", "Weight increase/decrease", "Shape deformation", "State change", "Uneven deformation", | |
| "Complex shape deformation", "Twisting/entwining", "Non-uniform expansion/contraction", "Rounded/sharpened edges", | |
| "Cracking/splitting", "Fragmentation", "Water resistance", "Dust resistance", "Denting/recovery", | |
| "Folding/unfolding", "Compression/expansion", "Stretching/contraction", "Wrinkling/flattening", "Crushing/hardening", | |
| "Rolling/unrolling", "Bending/curving" | |
| ], | |
| # 이하 동일 패턴으로 영문 버전 생략 (상동) | |
| } | |
| # ──────────────────────────────── Logging ──────────────────────────────── | |
| logging.basicConfig(level=logging.INFO, | |
| format="%(asctime)s - %(levelname)s - %(message)s") | |
| # ──────────────────────────────── OpenAI Client ────────────────────────── | |
| def get_openai_client(): | |
| """Create an OpenAI client with timeout and retry settings.""" | |
| if not OPENAI_API_KEY: | |
| raise RuntimeError("⚠️ OPENAI_API_KEY 환경 변수가 설정되지 않았습니다.") | |
| return OpenAI( | |
| api_key=OPENAI_API_KEY, | |
| timeout=60.0, # 타임아웃 60초로 설정 | |
| max_retries=3 # 재시도 횟수 3회로 설정 | |
| ) | |
| # ──────────────────────────────── New System Prompt for Idea Generation ───────────────────── | |
| def get_idea_system_prompt() -> str: | |
| """ | |
| 아이디어 생성용 시스템 프롬프트: | |
| - 제공된 physical_transformation_categories를 활용 | |
| - 3가지 발상 접근: (1) 유사성 기반 증강, (2) 반유사성(대조) 기반 발상의 전환, (3) 무작위(우연적 착안) | |
| - 각 아이디어를 논리적으로 설명 가능하도록 지시 | |
| - 아이디어마다 이미지 프롬프트도 함께 제시할 것 | |
| """ | |
| prompt = """ | |
| You are an advanced Idea Generation AI. Your goal is to take the user's prompt (topic, concept, or question) and create **3 distinct ideas** around it, drawing from the vast categories of **physical or conceptual transformations**. Specifically, you must produce: | |
| 1. **Similarity-based Augmentation** | |
| - Build upon the user's original concept by closely aligning to its core attributes. | |
| - Reference relevant categories or subcategories that naturally extend or amplify the original idea. | |
| - Provide a practical, logical explanation. | |
| 2. **Contrarian/Inverse Approach** | |
| - Intentionally invert or contradict the core assumptions of the original concept. | |
| - Use contrasting categories to spark unconventional thinking. | |
| - Clearly justify why this contrarian perspective might reveal novel possibilities. | |
| 3. **Random (Fortuitous Spark)** | |
| - Randomly select one or more categories or subcategories from any domain to introduce surprising or serendipitous ideas. | |
| - Show how this unexpected combination can yield fresh insights or opportunities. | |
| For each of the 3 approaches: | |
| - Provide a concise name or title for the idea. | |
| - Offer a clear, structured explanation (a few paragraphs) on how this idea works or could be realized. | |
| - Incorporate references to the relevant category or categories from the provided classification, explaining how they link to the idea. | |
| - Suggest a short, single-line **English prompt for generating an image** that best represents that idea. | |
| Finally: | |
| - Return your answer in Markdown format with headings (e.g., "## Idea 1: ...", "## Idea 2: ...", "## Idea 3: ..."). | |
| - Do **not** mention that you are responding with "3 ideas" because of a prompt or instruction. Simply present them as the final structured output. | |
| - Never mention "blog," "SEO," or "prompt" instructions in your final text. | |
| - Do **not** mention that you're an AI. Present the content as a straightforward creative text for each idea. | |
| --- | |
| **Important**: You have access to a list of "physical_transformation_categories" (in Korean) and possibly an English version. Use them to anchor or inspire each idea, but do not simply copy the entire list. Instead, selectively reference a few relevant subcategories. | |
| """ | |
| return prompt.strip() | |
| # ──────────────────────────────── Brave Search API ──────────────────────── | |
| def brave_search(query: str, count: int = 20): | |
| """ | |
| Call the Brave Web Search API → list[dict] | |
| Returns fields: index, title, link, snippet, displayed_link | |
| """ | |
| if not BRAVE_KEY: | |
| raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) 환경 변수가 비어있습니다.") | |
| headers = { | |
| "Accept": "application/json", | |
| "Accept-Encoding": "gzip", | |
| "X-Subscription-Token": BRAVE_KEY | |
| } | |
| params = {"q": query, "count": str(count)} | |
| for attempt in range(3): | |
| try: | |
| r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15) | |
| r.raise_for_status() | |
| data = r.json() | |
| logging.info(f"Brave search result data structure: {list(data.keys())}") | |
| raw = data.get("web", {}).get("results") or data.get("results", []) | |
| if not raw: | |
| logging.warning(f"No Brave search results found. Response: {data}") | |
| raise ValueError("No search results found.") | |
| arts = [] | |
| for i, res in enumerate(raw[:count], 1): | |
| url = res.get("url", res.get("link", "")) | |
| host = re.sub(r"https?://(www\.)?", "", url).split("/")[0] | |
| arts.append({ | |
| "index": i, | |
| "title": res.get("title", "No title"), | |
| "link": url, | |
| "snippet": res.get("description", res.get("text", "No snippet")), | |
| "displayed_link": host | |
| }) | |
| logging.info(f"Brave search success: {len(arts)} results") | |
| return arts | |
| except Exception as e: | |
| logging.error(f"Brave search failure (attempt {attempt+1}/3): {e}") | |
| if attempt < 2: | |
| time.sleep(2) | |
| return [] | |
| def mock_results(query: str) -> str: | |
| """Fallback if search API fails""" | |
| ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| return (f"# Fallback Search Content (Generated: {ts})\n\n" | |
| f"The web search API request failed. Please generate the ideas based on general knowledge about '{query}'.\n\n" | |
| f"You may consider aspects such as:\n\n" | |
| f"- Basic definition or concept of {query}\n" | |
| f"- Commonly known facts or challenges\n" | |
| f"- Potential categories from the transformation list\n\n" | |
| f"Note: This is fallback guidance, not real-time data.\n\n") | |
| def do_web_search(query: str) -> str: | |
| """Perform web search and format the results.""" | |
| try: | |
| arts = brave_search(query, 20) | |
| if not arts: | |
| logging.warning("No search results, using fallback content") | |
| return mock_results(query) | |
| hdr = "# Web Search Results\nUse the information below to inspire or validate your ideas.\n\n" | |
| body = "\n".join( | |
| f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n" | |
| f"**Source**: [{a['displayed_link']}]({a['link']})\n\n---\n" | |
| for a in arts | |
| ) | |
| return hdr + body | |
| except Exception as e: | |
| logging.error(f"Web search process failed: {str(e)}") | |
| return mock_results(query) | |
| # ──────────────────────────────── File Upload Handling ───────────────────── | |
| def process_text_file(file): | |
| """Handle text file""" | |
| try: | |
| content = file.read() | |
| file.seek(0) | |
| text = content.decode('utf-8', errors='ignore') | |
| if len(text) > 10000: | |
| text = text[:9700] + "...(truncated)..." | |
| result = f"## Text File: {file.name}\n\n" | |
| result += text | |
| return result | |
| except Exception as e: | |
| logging.error(f"Error processing text file: {str(e)}") | |
| return f"Error processing text file: {str(e)}" | |
| def process_csv_file(file): | |
| """Handle CSV file""" | |
| try: | |
| content = file.read() | |
| file.seek(0) | |
| df = pd.read_csv(io.BytesIO(content)) | |
| result = f"## CSV File: {file.name}\n\n" | |
| result += f"- Rows: {len(df)}\n" | |
| result += f"- Columns: {len(df.columns)}\n" | |
| result += f"- Column Names: {', '.join(df.columns.tolist())}\n\n" | |
| result += "### Data Preview\n\n" | |
| preview_df = df.head(10) | |
| try: | |
| markdown_table = preview_df.to_markdown(index=False) | |
| if markdown_table: | |
| result += markdown_table + "\n\n" | |
| else: | |
| result += "Unable to display CSV data.\n\n" | |
| except Exception as e: | |
| logging.error(f"Markdown table conversion error: {e}") | |
| result += "Displaying data as text:\n\n" | |
| result += str(preview_df) + "\n\n" | |
| num_cols = df.select_dtypes(include=['number']).columns | |
| if len(num_cols) > 0: | |
| result += "### Basic Statistical Information\n\n" | |
| try: | |
| stats_df = df[num_cols].describe().round(2) | |
| stats_markdown = stats_df.to_markdown() | |
| if stats_markdown: | |
| result += stats_markdown + "\n\n" | |
| else: | |
| result += "Unable to display statistical information.\n\n" | |
| except Exception as e: | |
| logging.error(f"Statistical info conversion error: {e}") | |
| result += "Unable to generate statistical information.\n\n" | |
| return result | |
| except Exception as e: | |
| logging.error(f"CSV file processing error: {str(e)}") | |
| return f"Error processing CSV file: {str(e)}" | |
| def process_pdf_file(file): | |
| """Handle PDF file""" | |
| try: | |
| file_bytes = file.read() | |
| file.seek(0) | |
| pdf_file = io.BytesIO(file_bytes) | |
| reader = PyPDF2.PdfReader(pdf_file, strict=False) | |
| result = f"## PDF File: {file.name}\n\n" | |
| result += f"- Total pages: {len(reader.pages)}\n\n" | |
| max_pages = min(5, len(reader.pages)) | |
| all_text = "" | |
| for i in range(max_pages): | |
| try: | |
| page = reader.pages[i] | |
| page_text = page.extract_text() | |
| current_page_text = f"### Page {i+1}\n\n" | |
| if page_text and len(page_text.strip()) > 0: | |
| if len(page_text) > 1500: | |
| current_page_text += page_text[:1500] + "...(truncated)...\n\n" | |
| else: | |
| current_page_text += page_text + "\n\n" | |
| else: | |
| current_page_text += "(No text could be extracted)\n\n" | |
| all_text += current_page_text | |
| if len(all_text) > 8000: | |
| all_text += "...(truncating remaining pages; PDF is too large)...\n\n" | |
| break | |
| except Exception as page_err: | |
| logging.error(f"Error processing PDF page {i+1}: {str(page_err)}") | |
| all_text += f"### Page {i+1}\n\n(Error extracting content: {str(page_err)})\n\n" | |
| if len(reader.pages) > max_pages: | |
| all_text += f"\nNote: Only the first {max_pages} pages are shown out of {len(reader.pages)} total.\n\n" | |
| result += "### PDF Content\n\n" + all_text | |
| return result | |
| except Exception as e: | |
| logging.error(f"PDF file processing error: {str(e)}") | |
| return f"## PDF File: {file.name}\n\nError occurred: {str(e)}\n\nThis PDF file cannot be processed." | |
| def process_uploaded_files(files): | |
| """Combine the contents of all uploaded files into one string.""" | |
| if not files: | |
| return None | |
| result = "# Uploaded File Contents\n\n" | |
| result += "Below is the content from the files provided by the user. Integrate this data as needed for generating ideas.\n\n" | |
| for file in files: | |
| try: | |
| ext = file.name.split('.')[-1].lower() | |
| if ext == 'txt': | |
| result += process_text_file(file) + "\n\n---\n\n" | |
| elif ext == 'csv': | |
| result += process_csv_file(file) + "\n\n---\n\n" | |
| elif ext == 'pdf': | |
| result += process_pdf_file(file) + "\n\n---\n\n" | |
| else: | |
| result += f"### Unsupported File: {file.name}\n\n---\n\n" | |
| except Exception as e: | |
| logging.error(f"File processing error {file.name}: {e}") | |
| result += f"### File processing error: {file.name}\n\nError: {e}\n\n---\n\n" | |
| return result | |
| # ──────────────────────────────── Image & Utility ───────────────────────── | |
| def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3): | |
| """Image generation function.""" | |
| if not prompt: | |
| return None, "Insufficient prompt" | |
| try: | |
| res = Client(IMAGE_API_URL).predict( | |
| prompt=prompt, width=w, height=h, guidance=g, | |
| inference_steps=steps, seed=seed, | |
| do_img2img=False, init_image=None, | |
| image2image_strength=0.8, resize_img=True, | |
| api_name="/generate_image" | |
| ) | |
| return res[0], f"Seed: {res[1]}" | |
| except Exception as e: | |
| logging.error(e) | |
| return None, str(e) | |
| def md_to_html(md: str, title="Idea Output"): | |
| """Convert Markdown to HTML.""" | |
| return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>" | |
| def keywords(text: str, top=5): | |
| """Simple keyword extraction (for web search).""" | |
| cleaned = re.sub(r"[^가-힣a-zA-Z0-9\s]", "", text) | |
| return " ".join(cleaned.split()[:top]) | |
| # ──────────────────────────────── Streamlit UI ──────────────────────────── | |
| def idea_generator_app(): | |
| st.title("Creative Idea Generator") | |
| # Set default session state | |
| if "ai_model" not in st.session_state: | |
| st.session_state.ai_model = "gpt-4.1-mini" | |
| if "messages" not in st.session_state: | |
| st.session_state.messages = [] | |
| if "auto_save" not in st.session_state: | |
| st.session_state.auto_save = True | |
| if "generate_image" not in st.session_state: | |
| st.session_state.generate_image = True # 기본값: True | |
| if "web_search_enabled" not in st.session_state: | |
| st.session_state.web_search_enabled = True | |
| # Sidebar UI | |
| sb = st.sidebar | |
| sb.title("Idea Generator Settings") | |
| sb.toggle("Auto Save", key="auto_save") | |
| sb.toggle("Auto Image Generation", key="generate_image") | |
| web_search_enabled = sb.toggle("Use Web Search", value=st.session_state.web_search_enabled) | |
| st.session_state.web_search_enabled = web_search_enabled | |
| if web_search_enabled: | |
| sb.info("✅ Web search results will be integrated.") | |
| # 예시 주제들 (원래 예시 블로그 토픽 -> 이제는 예시 아이디어 주제로 전환) | |
| example_topics = { | |
| "example1": "AI로 인한 비즈니스 모델 변화", | |
| "example2": "스마트 소재를 활용한 친환경 건축", | |
| "example3": "미래형 교육 서비스 구상" | |
| } | |
| sb.subheader("Example Prompts") | |
| c1, c2, c3 = sb.columns(3) | |
| if c1.button("AI & 비즈니스", key="ex1"): | |
| process_example(example_topics["example1"]) | |
| if c2.button("스마트 소재 건축", key="ex2"): | |
| process_example(example_topics["example2"]) | |
| if c3.button("미래형 교육", key="ex3"): | |
| process_example(example_topics["example3"]) | |
| # Download the latest ideas | |
| latest_ideas = next( | |
| (m["content"] for m in reversed(st.session_state.messages) | |
| if m["role"] == "assistant" and m["content"].strip()), | |
| None | |
| ) | |
| if latest_ideas: | |
| title_match = re.search(r"# (.*?)(\n|$)", latest_ideas) | |
| title = title_match.group(1).strip() if title_match else "ideas" | |
| sb.subheader("Download Latest Ideas") | |
| d1, d2 = sb.columns(2) | |
| d1.download_button("Download as Markdown", latest_ideas, | |
| file_name=f"{title}.md", mime="text/markdown") | |
| d2.download_button("Download as HTML", md_to_html(latest_ideas, title), | |
| file_name=f"{title}.html", mime="text/html") | |
| # JSON conversation record upload | |
| up = sb.file_uploader("Load Conversation History (.json)", type=["json"], key="json_uploader") | |
| if up: | |
| try: | |
| st.session_state.messages = json.load(up) | |
| sb.success("Conversation history loaded successfully") | |
| except Exception as e: | |
| sb.error(f"Failed to load: {e}") | |
| # JSON conversation record download | |
| if sb.button("Download Conversation as JSON"): | |
| sb.download_button( | |
| "Save JSON", | |
| data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2), | |
| file_name="chat_history.json", | |
| mime="application/json" | |
| ) | |
| # File Upload | |
| st.subheader("File Upload (Optional)") | |
| uploaded_files = st.file_uploader( | |
| "Upload files to reference in the idea generation (txt, csv, pdf)", | |
| type=["txt", "csv", "pdf"], | |
| accept_multiple_files=True, | |
| key="file_uploader" | |
| ) | |
| if uploaded_files: | |
| file_count = len(uploaded_files) | |
| st.success(f"{file_count} files uploaded.") | |
| with st.expander("Preview Uploaded Files", expanded=False): | |
| for idx, file in enumerate(uploaded_files): | |
| st.write(f"**File Name:** {file.name}") | |
| ext = file.name.split('.')[-1].lower() | |
| if ext == 'txt': | |
| preview = file.read(1000).decode('utf-8', errors='ignore') | |
| file.seek(0) | |
| st.text_area( | |
| f"Preview of {file.name}", | |
| preview + ("..." if len(preview) >= 1000 else ""), | |
| height=150 | |
| ) | |
| elif ext == 'csv': | |
| try: | |
| df = pd.read_csv(file) | |
| file.seek(0) | |
| st.write("CSV Preview (up to 5 rows)") | |
| st.dataframe(df.head(5)) | |
| except Exception as e: | |
| st.error(f"CSV preview failed: {e}") | |
| elif ext == 'pdf': | |
| try: | |
| file_bytes = file.read() | |
| file.seek(0) | |
| pdf_file = io.BytesIO(file_bytes) | |
| reader = PyPDF2.PdfReader(pdf_file, strict=False) | |
| pc = len(reader.pages) | |
| st.write(f"PDF File: {pc} pages") | |
| if pc > 0: | |
| try: | |
| page_text = reader.pages[0].extract_text() | |
| preview = page_text[:500] if page_text else "(No text)" | |
| st.text_area("Preview of the first page", preview + "...", height=150) | |
| except: | |
| st.warning("Failed to extract text from the first page") | |
| except Exception as e: | |
| st.error(f"PDF preview failed: {e}") | |
| if idx < file_count - 1: | |
| st.divider() | |
| # Display existing messages in chat | |
| for m in st.session_state.messages: | |
| with st.chat_message(m["role"]): | |
| st.markdown(m["content"]) | |
| if "image" in m: | |
| st.image(m["image"], caption=m.get("image_caption", "")) | |
| # User input for idea generation | |
| prompt = st.chat_input("Enter a topic or concept to generate 3 new ideas.") | |
| if prompt: | |
| process_input(prompt, uploaded_files) | |
| # Sidebar footer | |
| sb.markdown("---") | |
| sb.markdown("Created by [Ginigen.com](https://ginigen.com) | [YouTube](https://www.youtube.com/@ginipickaistudio)") | |
| def process_example(topic): | |
| """Handle example prompts.""" | |
| process_input(topic, []) | |
| def process_input(prompt: str, uploaded_files): | |
| # Add user's message | |
| if not any(m["role"] == "user" and m["content"] == prompt for m in st.session_state.messages): | |
| st.session_state.messages.append({"role": "user", "content": prompt}) | |
| with st.chat_message("user"): | |
| st.markdown(prompt) | |
| with st.chat_message("assistant"): | |
| placeholder = st.empty() | |
| message_placeholder = st.empty() | |
| full_response = "" | |
| use_web_search = st.session_state.web_search_enabled | |
| has_uploaded_files = bool(uploaded_files) and len(uploaded_files) > 0 | |
| try: | |
| status = st.status("Preparing to generate ideas...") | |
| status.update(label="Initializing client...") | |
| client = get_openai_client() | |
| # Prepare system prompt | |
| sys_prompt = get_idea_system_prompt() | |
| # Optional: gather search results | |
| search_content = None | |
| if use_web_search: | |
| status.update(label="Performing web search...") | |
| with st.spinner("Searching the web..."): | |
| search_content = do_web_search(keywords(prompt, top=5)) | |
| # File contents | |
| file_content = None | |
| if has_uploaded_files: | |
| status.update(label="Processing uploaded files...") | |
| with st.spinner("Analyzing files..."): | |
| file_content = process_uploaded_files(uploaded_files) | |
| # Build messages | |
| user_content = prompt | |
| if search_content: | |
| user_content += "\n\n" + search_content | |
| if file_content: | |
| user_content += "\n\n" + file_content | |
| api_messages = [ | |
| {"role": "system", "content": sys_prompt}, | |
| {"role": "user", "content": user_content} | |
| ] | |
| # OpenAI API streaming | |
| status.update(label="Generating creative ideas...") | |
| try: | |
| stream = client.chat.completions.create( | |
| model="gpt-4.1-mini", | |
| messages=api_messages, | |
| temperature=1, | |
| max_tokens=MAX_TOKENS, | |
| top_p=1, | |
| stream=True | |
| ) | |
| for chunk in stream: | |
| if (chunk.choices | |
| and len(chunk.choices) > 0 | |
| and chunk.choices[0].delta.content is not None): | |
| content_delta = chunk.choices[0].delta.content | |
| full_response += content_delta | |
| message_placeholder.markdown(full_response + "▌") | |
| message_placeholder.markdown(full_response) | |
| status.update(label="Ideas created!", state="complete") | |
| except Exception as api_error: | |
| error_message = str(api_error) | |
| logging.error(f"API error: {error_message}") | |
| status.update(label=f"Error: {error_message}", state="error") | |
| raise Exception(f"Idea generation error: {error_message}") | |
| # Store final text | |
| answer_entry_saved = False | |
| # 자동 이미지 생성이 활성화되어 있다면 | |
| if st.session_state.generate_image and full_response: | |
| # 아이디어를 3개로 구분하고, 각 아이디어마다 이미지 프롬프트를 추출 | |
| # 간단한 방식: 정규식으로 `Image Prompt:` 형태를 찾는다고 가정 (시스템프롬프트에 지시) | |
| # 혹은 단순히 한 번만 추출 -> 여기서는 3개 아이디어 각각을 찾기 위해 나눠본다. | |
| # 일단은 전체 답변에서 "English prompt for generating an image" 부분을 찾는다. | |
| # 매우 단순한 파싱 예시 (개선 가능) | |
| idea_sections = re.split(r"(## Idea \d+:)", full_response) | |
| # idea_sections는 ['', '## Idea 1:', ' ...', '## Idea 2:', ' ...', '## Idea 3:', ' ...'] 형태 | |
| # 다시 묶어서 [('## Idea 1:', idea_text), ('## Idea 2:', idea_text), ...] 형태로 | |
| pairs = [] | |
| for i in range(1, len(idea_sections), 2): | |
| idea_title = idea_sections[i].strip() | |
| idea_body = idea_sections[i+1].strip() if i+1 < len(idea_sections) else "" | |
| pairs.append((idea_title, idea_body)) | |
| # 각 아이디어마다 이미지 생성 | |
| for idx, (title, text_block) in enumerate(pairs, start=1): | |
| image_prompt_match = re.search(r"(?i)(image prompt\s*\:\s*)(.+)", text_block) | |
| if image_prompt_match: | |
| raw_prompt = image_prompt_match.group(2).strip() | |
| # 만약 문장 끝에 불필요한 구두점이나 줄바꿈이 있을 시 제거 | |
| raw_prompt = re.sub(r"[\r\n]+", " ", raw_prompt) | |
| raw_prompt = re.sub(r"[\"'`]", "", raw_prompt) | |
| # 이미지 생성 | |
| with st.spinner(f"Generating image for {title}..."): | |
| img, cap = generate_image(raw_prompt) | |
| if img: | |
| st.image(img, caption=f"{title} - {cap}") | |
| # 대화에 저장 | |
| st.session_state.messages.append({ | |
| "role": "assistant", | |
| "content": "", | |
| "image": img, | |
| "image_caption": f"{title} - {cap}" | |
| }) | |
| # 3개 이미지 생성 프로세스를 마친 후 최종 텍스트를 저장 | |
| st.session_state.messages.append({"role": "assistant", "content": full_response}) | |
| answer_entry_saved = True | |
| if not answer_entry_saved and full_response: | |
| # 이미지 생성이 비활성화거나 실패했을 경우 텍스트만 저장 | |
| st.session_state.messages.append({"role": "assistant", "content": full_response}) | |
| # Download buttons | |
| if full_response: | |
| st.subheader("Download This Output") | |
| c1, c2 = st.columns(2) | |
| c1.download_button( | |
| "Markdown", | |
| data=full_response, | |
| file_name=f"{prompt[:30]}.md", | |
| mime="text/markdown" | |
| ) | |
| c2.download_button( | |
| "HTML", | |
| data=md_to_html(full_response, prompt[:30]), | |
| file_name=f"{prompt[:30]}.html", | |
| mime="text/html" | |
| ) | |
| # Auto-save | |
| if st.session_state.auto_save and st.session_state.messages: | |
| try: | |
| fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json" | |
| with open(fn, "w", encoding="utf-8") as fp: | |
| json.dump(st.session_state.messages, fp, ensure_ascii=False, indent=2) | |
| except Exception as e: | |
| logging.error(f"Auto-save failed: {e}") | |
| except Exception as e: | |
| error_message = str(e) | |
| placeholder.error(f"An error occurred: {error_message}") | |
| logging.error(f"Process input error: {error_message}") | |
| ans = f"An error occurred while processing your request: {error_message}" | |
| st.session_state.messages.append({"role": "assistant", "content": ans}) | |
| # ──────────────────────────────── main ──────────────────────────────────── | |
| def main(): | |
| idea_generator_app() | |
| if __name__ == "__main__": | |
| main() | |