Spaces:
Running
Running
# ──────────────────────────────── Imports ──────────────────────────────── | |
import os, json, re, logging, requests, markdown, time, io | |
from datetime import datetime | |
import streamlit as st | |
st.set_page_config(layout="wide") | |
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" | |
], | |
"Surface and Appearance Change": [ | |
"Color change", "Texture change", "Transparency change", "Glossy/matte change", | |
"Light reflection variation", "Pattern change", "Angle-dependent color change", "Light-induced color change", | |
"Temperature-dependent color change", "Holographic effect", "Angle-specific light reflection", "Surface shape alteration", | |
"Nano-scale surface structure change", "Self-cleaning effect", "Stain/pattern formation", "Blurriness/clarity change", | |
"Luster/shine change", "Hue/saturation change", "Luminescence/fluorescence", "Light scattering effect", | |
"Light absorption change", "Translucency effect", "Shadow effect change", "UV response change", | |
"Glow effect" | |
], | |
"Material State Change": [ | |
"Solid/liquid/gas transition", "Crystallization/dissolution", "Oxidation/corrosion", "Hardening/softening", | |
"Special state transition", "Amorphous/crystalline transition", "Component separation", "Particle formation/disintegration", | |
"Gel formation/dissolution", "Metastable state change", "Molecular self-assembly/disintegration", "Delayed state change", | |
"Melting", "Solidification", "Evaporation/condensation", "Sublimation/deposition", "Precipitation/suspension", "Dispersion/aggregation", | |
"Drying/moistening", "Swelling/shrinkage", "Freezing/thawing", "Weathering/erosion", "Charging/discharging", | |
"Bonding/separation", "Fermentation/decay" | |
], | |
"Movement Characteristics Change": [ | |
"Acceleration/deceleration", "Maintaining constant speed", "Vibration/vibration reduction", "Collision/bouncing", | |
"Increase/decrease in rotational speed", "Change in rotational direction", "Irregular movement", "Stop-and-slide phenomenon", | |
"Resonance/anti-resonance", "Resistance/lift change in fluid", "Change in movement resistance", "Complex vibrational movement", | |
"Movement in special fluid", "Rotational-translational movement", "Inertial stoppage", "Shock absorption", | |
"Shock transfer", "Conservation of momentum", "Friction change", "Overcoming inertia", "Unstable equilibrium", | |
"Dynamic stability", "Damping of oscillation", "Path predictability", "Evasive movement" | |
], | |
"Structural Change": [ | |
"Addition/removal of components", "Assembly/disassembly", "Folding/unfolding", "Deformation/recovery", "Optimal structural change", | |
"Self-rearrangement", "Natural pattern formation/disappearance", "Regular pattern change", "Modular transformation", | |
"Increased structural complexity", "Memory of original shape effect", "Shape change over time", "Partial removal", | |
"Partial replacement", "Bonding", "Separation", "Division/integration", "Overlaying", "Internal structure change", | |
"External structure change", "Shift of center axis", "Balance point change", "Hierarchical structure change", "Support structure change", | |
"Stress distribution structure", "Shock absorption structure", "Grid/matrix structure change", "Interconnectivity change" | |
], | |
"Spatial Movement": [ | |
"Forward/backward movement", "Left/right movement", "Up/down movement", "Vertical axis rotation (nodding)", | |
"Horizontal axis rotation (shaking head)", "Longitudinal axis rotation (tilting sideways)", "Circular motion", "Spiral movement", | |
"Slipping due to inertia", "Change of rotation axis", "Irregular rotation", "Shaking movement", "Parabolic motion", | |
"Zero-gravity floating", "Floating on water surface", "Jump/leap", "Sliding", "Rolling", "Free fall", | |
"Reciprocating motion", "Elastic bouncing", "Penetration", "Evasive movement", "Zigzag movement", "Swinging movement" | |
], | |
"Time-Related Change": [ | |
"Aging/weathering", "Wear/corrosion", "Fading/discoloration", "Damage/recovery", "Lifecycle change", | |
"Adaptation through user interaction", "Learning-based shape optimization", "Property change over time", | |
"Collective memory effect", "Cultural significance change", "Delayed response", "History-dependent change", | |
"Gradual time change", "Evolutionary change", "Periodic regeneration", "Seasonal adaptation", | |
"Circadian rhythm change", "Lifecycle stage", "Growth/decline", "Self-repair/regeneration", | |
"Natural cycle adaptation", "Persistence/transience", "Memory effect", "Delayed effect", "Cumulative effect" | |
], | |
"Light and Visual Effects": [ | |
"Illumination/shutdown", "Light transmission/blocking", "Light scattering/concentration", "Color spectrum change", "Light diffraction", | |
"Light interference", "Hologram creation", "Laser effect", "Light polarization", "Fluorescence/phosphorescence", | |
"UV/IR emission", "Optical illusion", "Light refraction", "Shadow creation/removal", | |
"Chromatic aberration", "Rainbow effect", "Glow effect", "Flash effect", "Lighting pattern", | |
"Beam effect", "Light filter effect", "Change in light direction", "Projection effect", "Light detection/response", | |
"Luminance change" | |
], | |
"Sound and Vibration Effects": [ | |
"Sound generation/cessation", "Pitch change", "Volume change", "Timbre change", | |
"Resonance/antiresonance", "Acoustic vibration", "Ultrasonic/infrasonic emission", "Sound concentration/distribution", | |
"Sound reflection/absorption", "Acoustic Doppler effect", "Sound wave interference", "Acoustic resonance", | |
"Vibration pattern change", "Percussive effect", "Audio feedback", "Sound shielding/amplification", | |
"Directional sound", "Sound distortion", "Beat generation", "Harmonics generation", "Frequency modulation", | |
"Acoustic shockwave", "Sound filtering" | |
], | |
"Thermal Changes": [ | |
"Temperature rise/fall", "Thermal expansion/contraction", "Heat transfer/blocking", "Pressure increase/decrease", | |
"Magnetization due to heat change", "Entropy change", "Thermoelectric effect", "Magnetic-induced thermal change", | |
"Heat storage/release during phase change", "Thermal stress buildup/release", "Impact of rapid temperature change", | |
"Radiative cooling/heating", "Exothermic/endothermic", "Heat distribution change", "Heat reflection/absorption", | |
"Cooling condensation", "Thermal activation", "Thermal discoloration", "Coefficient of thermal expansion change", "Thermal stability change", | |
"Heat resistance/cold resistance", "Self-heating", "Thermal equilibrium/imbalance", "Thermal deformation", "Heat dispersion/concentration" | |
], | |
"Electrical and Magnetic Changes": [ | |
"Magnetism creation/cessation", "Charge increase/decrease", "Electric field creation/cessation", "Magnetic field creation/cessation", | |
"Superconducting transition", "Ferroelectric property change", "Quantum state change", "Plasma formation/cessation", | |
"Spin wave transmission", "Electricity generation by light", "Electricity generation by pressure", "Current change in magnetic field", | |
"Electrical resistance change", "Electrical conductivity change", "Static electricity generation/discharge", "Electromagnetic induction", | |
"Electromagnetic wave emission/absorption", "Capacitance change", "Magnetic hysteresis", "Electrical polarization", | |
"Electron flow direction change", "Electrical resonance", "Electrical shielding/exposure", "Magnetic shielding/exposure", | |
"Magnetic field alignment" | |
], | |
"Chemical Change": [ | |
"Surface coating change", "Material composition change", "Chemical reaction change", "Catalytic action start/stop", | |
"Light-induced chemical reaction", "Electricity-induced chemical reaction", "Monolayer formation", "Molecular-level structural change", | |
"Biomimetic surface change", "Environmentally responsive material change", "Periodic chemical reaction", "Oxidation", "Reduction", | |
"Polymerization", "Water splitting", "Compound formation", "Radiation effects", "Acid-base reaction", "Neutralization reaction", | |
"Ionization", "Chemical adsorption/desorption", "Catalytic efficiency change", "Enzyme activity change", "Colorimetric reaction", | |
"pH change", "Chemical equilibrium shift", "Bond formation/breakage", "Solubility change" | |
], | |
"Biological Change": [ | |
"Growth/shrinkage", "Cell division/death", "Bioluminescence", "Metabolic change", "Immune response", | |
"Hormone secretion", "Neural response", "Genetic expression", "Adaptation/evolution", "Circadian rhythm change", | |
"Regeneration/healing", "Aging/maturation", "Biomimetic change", "Biofilm formation", "Biological degradation", | |
"Enzyme activation/inactivation", "Biological signaling", "Stress response", "Thermoregulation", "Biological clock change", | |
"Extracellular matrix change", "Biomechanical response", "Cell motility", "Cell polarity change", "Nutritional status change" | |
], | |
"Environmental Interaction": [ | |
"Temperature response", "Humidity response", "Pressure response", "Gravity response", "Magnetic field response", | |
"Light response", "Sound response", "Chemical detection", "Mechanical stimulus detection", "Electrical stimulus response", | |
"Radiation response", "Vibration detection", "pH response", "Solvent response", "Gas exchange", | |
"Pollution response", "Weather response", "Seasonal response", "Circadian response", "Ecosystem interaction", | |
"Symbiotic/competitive interaction", "Predator/prey relationship", "Swarm formation", "Territorial behavior", "Migration/settlement pattern" | |
], | |
"Business Ideas": [ | |
"Market redefinition / New market creation", | |
"Business model innovation / Digital transformation", | |
"Customer experience innovation / Service innovation", | |
"Strengthened collaboration and partnerships / Ecosystem building", | |
"Global expansion / Localization strategy", | |
"Increased operational efficiency / Cost reduction", | |
"Brand repositioning / Image transformation", | |
"Sustainable growth / Social value creation", | |
"Data-driven decision making / AI adoption", | |
"Convergence of new technologies / Innovative investments" | |
] | |
} | |
# ──────────────────────────────── 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 ───────────────────── | |
# ───────────────── System Prompt (REPORT STYLE) ───────────────── | |
def get_idea_system_prompt(selected_category: str | None = None) -> str: | |
""" | |
새로운 CCM(크로스 카테고리 매트릭스) 방법론을 적용한 시스템 프롬프트 | |
""" | |
cat_clause = ( | |
f'\n**추가 지침**: 선택된 카테고리 "{selected_category}"에 특별한 주의를 기울이십시오. ' | |
f'이 카테고리의 항목들을 2단계와 3단계 모두에서 우선적으로 고려하십시오.\n' | |
) if selected_category else "" | |
prompt = f""" | |
반드시 한글(한국어)로 답변하라. 당신은 혁신 컨설턴트로서 CCM(크로스 카테고리 매트릭스) 방법론을 활용하여 창의적 아이디어를 도출합니다. | |
### CCM 방법론 프로세스 | |
다음 단계를 통해 체계적으로 아이디어를 도출하십시오: | |
1. **문제 분석 및 지식 베이스 구축** | |
- 입력된 프롬프트의 요구사항을 명확히 정의하고 핵심 니즈 추출 | |
- 웹검색 결과를 분석하여 관련 트렌드, 기술, 사례 파악 | |
- 문제 해결에 필요한 핵심 기능과 속성 식별 | |
2. **카테고리별 적합성 매핑** | |
- 모든 카테고리에서 문제 해결에 적합한 항목들을 식별 | |
- 각 카테고리별로 최소 1개 이상의 관련 항목 선정 | |
- 선정 항목과 문제 간의 연결성 설명 | |
3. **랜덤 크로스 매트릭스 생성** | |
- 16개 카테고리 중 6개를 무작위로 선정 | |
- 선정된 각 카테고리에서 1개 항목을 추출 | |
- 이 6개 항목을 조합하여 통합적 솔루션 도출 | |
### 출력 형식 | |
최종 보고서를 다음 구조로 마크다운 형식으로 작성하십시오: | |
## 1️⃣ 문제 분석 및 지식 베이스 | |
- **문제 정의**: [프롬프트 핵심 요구사항 요약] | |
- **핵심 니즈**: [불릿 3-5개로 주요 니즈 리스트] | |
- **현황 분석**: [웹검색이나 배경지식 기반 현재 상황 요약] | |
## 2️⃣ 카테고리별 적합성 매핑 | |
[각 카테고리별로 가장 적합한 항목들을 표 형식으로 정리] | |
| 카테고리 | 적합 항목 | 연결성 | | |
|---------|----------|--------| | |
| [카테고리1] | [선정 항목] | [문제와의 연결성 설명] | | |
| [카테고리2] | [선정 항목] | [문제와의 연결성 설명] | | |
(최소 5개 카테고리) | |
## 3️⃣ CCM 통합 솔루션 | |
### 랜덤 선정 카테고리/항목 | |
[무작위 선정된 6개 카테고리와 항목을 표로 정리] | |
| 선정 카테고리 | 선정 항목 | | |
|--------------|----------| | |
| [카테고리1] | [항목1] | | |
| [카테고리2] | [항목2] | | |
... | |
### 최종 아이디어: [아이디어 제목] | |
- **개념 요약**: [2-3문장으로 핵심 아이디어 설명] | |
- **상세 설명**: [3-4 문단으로 아이디어 구체화] | |
- **구현 방식**: [아이디어 실현을 위한 단계/방법] | |
- **차별화 요소**: [선정된 6개 항목이 어떻게 시너지를 내는지 설명] | |
### 기대 효과 | |
- [불릿 포인트로 3-5개 주요 기대효과 나열] | |
### 한계 및 도전 과제 | |
- [불릿 포인트로 2-3개 잠재적 한계/도전 제시] | |
### 이미지 프롬프트 | |
[아이디어를 시각화할 수 있는 영문 이미지 프롬프트 1줄] | |
{cat_clause} | |
Step-by-step 사고 과정을 따르되, 출력에는 최종 보고서만 표시하십시오. | |
""" | |
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": "도시 물 부족 문제 해결을 위한 혁신적 방안", | |
"example2": "노인 돌봄 서비스의 디지털 전환", | |
"example3": "지속가능한 식품 포장 솔루션" | |
} | |
sb.subheader("Category Focus (Optional)") | |
category_keys = ["(None)"] + list(physical_transformation_categories.keys()) | |
sb.selectbox( | |
"Generate ideas mainly using this category", | |
options=category_keys, | |
key="category_focus", | |
index=0 # 기본값 "(None)" | |
) | |
sb.subheader("Example Prompts") | |
c1, c2, c3 = sb.columns(3) | |
if c1.button("도시 물 부족 문제", 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 write_output(md_text: str, prompt: str): | |
""" | |
• 대화 기록에 마크다운 답변 저장 | |
• 다운로드 버튼(마크다운·HTML) 제공 | |
• 자동 JSON 백업 | |
""" | |
# ① 채팅 기록에 추가 | |
st.session_state.messages.append({"role": "assistant", "content": md_text}) | |
# ② 다운로드 버튼 | |
st.subheader("Download This Output") | |
col_md, col_html = st.columns(2) | |
col_md.download_button( | |
label="Markdown", | |
data=md_text, | |
file_name=f"{prompt[:30]}.md", | |
mime="text/markdown" | |
) | |
col_html.download_button( | |
label="HTML", | |
data=md_to_html(md_text, prompt[:30]), | |
file_name=f"{prompt[:30]}.html", | |
mime="text/html" | |
) | |
# ③ 자동 JSON 저장 | |
if st.session_state.auto_save: | |
fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json" | |
try: | |
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}") | |
# ──────────────────────────────── process_input ──────────────────────────── | |
def process_input(prompt: str, uploaded_files): | |
""" | |
1) 사용자 입력을 GPT-4로 보내 창의적 아이디어 보고서 생성 | |
2) 선택적으로 이미지 생성 | |
3) 결과를 한 번만 기록·다운로드·백업 (중복 출력 방지) | |
""" | |
# 사용자 메시지 기록 | |
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) | |
# GPT 호출을 위한 초기화 | |
use_web_search = st.session_state.web_search_enabled | |
has_uploaded = bool(uploaded_files) | |
full_response = "" | |
with st.chat_message("assistant"): | |
# 프롬프트 출력 영역 (실시간 스트리밍) | |
message_placeholder = st.empty() | |
try: | |
with st.spinner("Preparing to generate ideas..."): | |
client = get_openai_client() | |
# 시스템 프롬프트 구성 | |
selected_cat = st.session_state.get("category_focus", "(None)") | |
if selected_cat == "(None)": | |
selected_cat = None | |
sys_prompt = get_idea_system_prompt(selected_category=selected_cat) | |
# 카테고리 정보 (JSON 형식) | |
def category_context(sel): | |
if sel: | |
return json.dumps( | |
{sel: physical_transformation_categories[sel]}, | |
ensure_ascii=False | |
) | |
return "ALL_CATEGORIES: " + ", ".join(physical_transformation_categories.keys()) | |
# 웹 검색 및 파일 내용 결합 | |
user_content = prompt | |
if use_web_search: | |
with st.spinner("Searching the web..."): | |
search_content = do_web_search(keywords(prompt, top=5)) | |
user_content += "\n\n" + search_content | |
if has_uploaded: | |
with st.spinner("Processing uploaded files..."): | |
file_content = process_uploaded_files(uploaded_files) | |
if file_content: | |
user_content += "\n\n" + file_content | |
# 대화 메시지 배열 | |
api_messages = [ | |
{"role": "system", "content": sys_prompt}, | |
{"role": "system", "name": "category_db", | |
"content": category_context(selected_cat)}, | |
{"role": "user", "content": user_content}, | |
] | |
# GPT-4 스트리밍 호출 | |
with st.spinner("Generating ideas..."): | |
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 chunk.choices[0].delta.get("content"): | |
token_text = chunk.choices[0].delta["content"] | |
full_response += token_text | |
message_placeholder.markdown(full_response + "▌") | |
# 최종 출력 | |
message_placeholder.markdown(full_response) | |
# 이미지 자동 생성 | |
if st.session_state.generate_image and full_response: | |
# 패턴1: "### 이미지 프롬프트" 형태 탐색 | |
ccm_match = re.search(r"###\s*이미지\s*프롬프트\s*\n+([^\n]+)", | |
full_response, flags=re.IGNORECASE) | |
# 패턴2: 예전 형식 "Image Prompt" 등 | |
legacy_match = None | |
if not ccm_match: | |
legacy_match = re.search( | |
r"\|\s*(?:\*\*)?Image\s+Prompt(?:\*\*)?\s*\|\s*([^|\n]+)", | |
full_response, flags=re.IGNORECASE | |
) or re.search( | |
r"(?i)Image\s+Prompt\s*[:\-]\s*([^\n]+)", | |
full_response | |
) | |
match = ccm_match or legacy_match | |
if match: | |
raw_prompt = re.sub(r"[\r\n`\"'\\]", " ", match.group(1)).strip() | |
with st.spinner("Generating idea image..."): | |
img, cap = generate_image(raw_prompt) | |
if img: | |
st.image(img, caption=f"아이디어 시각화 – {cap}") | |
st.session_state.messages.append({ | |
"role": "assistant", | |
"content": "", | |
"image": img, | |
"image_caption": f"아이디어 시각화 – {cap}" | |
}) | |
# 결과 기록·다운로드·백업 | |
write_output(full_response, prompt) | |
except Exception as e: | |
logging.error("Error in process_input", exc_info=True) | |
err_msg = f"⚠️ 오류가 발생했습니다: {e}" | |
st.error(err_msg) | |
st.session_state.messages.append({"role": "assistant", "content": err_msg}) | |
# ──────────────────────────────── main ──────────────────────────────────── | |
def main(): | |
idea_generator_app() | |
if __name__ == "__main__": | |
main() | |