├── Procfile ├── README.md ├── chatgpt.py ├── requirements.txt ├── runtime.txt └── setup.sh /Procfile: -------------------------------------------------------------------------------- 1 | web: sh setup.sh && streamlit run chatgpt.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ChatGPT를 활용한 블로그 자동 생성하기 2 | 3 | OpenAI API와 Streamlit으로 자동화된 블로그를 생성할 수 있는 데모 사이트를 만들어 봤습니다. 4 | 5 | Automated blog posts generator powered by ChatGPT and Streamlit 😀 6 | 7 | 아래의 데모 사이트에서 테스트 해볼 수 있습니다. 8 | 9 | **[데모 사이트 링크](https://bit.ly/chatgpt-blog)** 10 | -------------------------------------------------------------------------------- /chatgpt.py: -------------------------------------------------------------------------------- 1 | import re 2 | import zipfile 3 | import openai 4 | import streamlit as st 5 | import pandas as pd 6 | from datetime import datetime, timedelta 7 | 8 | def generate_text(prompt): 9 | # 모델 엔진 선택 10 | model_engine = "text-davinci-003" 11 | 12 | # 맥스 토큰 13 | max_tokens = 3500 14 | 15 | # 블로그 생성 16 | completion = openai.Completion.create( 17 | engine=model_engine, 18 | prompt=prompt, 19 | max_tokens=max_tokens, 20 | temperature=0.3, # creativity 21 | top_p=1, 22 | frequency_penalty=0, 23 | presence_penalty=0 24 | ) 25 | return completion 26 | 27 | def extract_tags(body): 28 | hashtag_pattern = r'(#+[a-zA-Z0-9(_)]{1,})' 29 | hashtags = [w[1:] for w in re.findall(hashtag_pattern, body)] 30 | hashtags = list(set(hashtags)) 31 | tag_string = "" 32 | for w in hashtags: 33 | # 3글자 이상 추출 34 | if len(w) > 3: 35 | tag_string += f'{w}, ' 36 | tag_string = re.sub(r'[^a-zA-Z, ]', '', tag_string) 37 | tag_string = tag_string.strip()[:-1] 38 | return tag_string 39 | 40 | def get_file(filename): 41 | with open(filename, 'r') as f: 42 | data = f.read() 43 | return data 44 | 45 | def make_prompt(prompt, topic='<>', category='<>'): 46 | if topic: 47 | prompt = prompt.replace('<>', topic) 48 | if category: 49 | prompt = prompt.replace('<>', category) 50 | return prompt 51 | 52 | def make_header(topic, category, tags): 53 | # 블로그 헤더 54 | page_head = f'''--- 55 | layout: single 56 | title: "{topic}" 57 | categories: {category} 58 | tag: [{tags}] 59 | toc: false 60 | author_profile: false 61 | ---''' 62 | return page_head 63 | 64 | prompt_example = f'''Write blog posts in markdown format. 65 | Write the theme of your blog as "<>" and its category is "<>". 66 | Highlight, bold, or italicize important words or sentences. 67 | Please include the restaurant's address, menu recommendations and other helpful information(opening and closing hours) as a list style. 68 | Please make the entire blog less than 10 minutes long. 69 | The audience of this article is 20-40 years old. 70 | Create several hashtags and add them only at the end of the line. 71 | Add a summary of the entire article at the beginning of the blog post.''' 72 | 73 | with st.sidebar: 74 | st.markdown(''' 75 | **API KEY 발급 방법** 76 | 1. https://beta.openai.com/ 회원가입 77 | 2. https://beta.openai.com/account/api-keys 접속 78 | 3. `create new secret key` 클릭 후 생성된 KEY 복사 79 | ''') 80 | value='' 81 | apikey = st.text_input(label='OPENAI API 키', placeholder='OPENAI API키를 입력해 주세요', value=value) 82 | 83 | if apikey: 84 | st.markdown(f'OPENAI API KEY: `{apikey}`') 85 | 86 | st.markdown('---') 87 | 88 | # Preset Container 89 | preset_container = st.container() 90 | preset_container.subheader('1. 설정') 91 | tab_single, tab_multiple = preset_container.tabs(['1개 생성', '여러개 생성']) 92 | 93 | col1, co12 = tab_single.columns(2) 94 | 95 | topic = col1.text_input(label='주제 입력', placeholder='주제를 입력해 주세요') 96 | col1.markdown('(예시)') 97 | col1.markdown('`Top 10 Restaurants you must visit when traveling to New York`') 98 | 99 | category = co12.text_input(label='카테고리 입력', placeholder='카테고리를 입력해 주세요') 100 | co12.markdown('(예시)') 101 | co12.markdown('`Travel`') 102 | 103 | def generate_blog(apikey, topic, category, prompt): 104 | # apikey 셋팅 105 | openai.api_key = apikey 106 | # prompt 생성 107 | prompt_output = make_prompt(prompt=prompt, topic=topic, category=category) 108 | # 글 생성 109 | response = generate_text(prompt_output) 110 | body = response.choices[0].text 111 | # 태그 생성 112 | tags = extract_tags(body) 113 | 114 | # header 생성 115 | header = make_header(topic=topic, category=category, tags=tags) 116 | # 첫 줄은 타이틀(제목)과 겹치기 때문에 제거하도록 합니다. 117 | body = '\n'.join(response['choices'][0]['text'].strip().split('\n')[1:]) 118 | # 최종 결과물 119 | output = header + body 120 | 121 | yesterday = datetime.now() - timedelta(days=1) 122 | timestring = yesterday.strftime('%Y-%m-%d') 123 | filename = f"{timestring}-{'-'.join(topic.lower().split())}.md" 124 | with open(filename, 'w') as f: 125 | f.write(output) 126 | f.close() 127 | return filename 128 | 129 | with tab_single: 130 | # Prompt Container 131 | prompt_container = st.container() 132 | prompt_container.subheader('2. 세부지침') 133 | prompt_container.markdown('[tip 1] **세부지침**은 [구글 번역기](https://translate.google.com/)로 돌려서 **영어로** 입력해 주세요') 134 | prompt_container.markdown('[tip 2] `<>`은 입력한 주제로 `<>`는 입력한 카테고리로 **치환**됩니다.') 135 | prompt_container.markdown('(예시)') 136 | prompt_container.markdown(f''' 137 | ``` 138 | {prompt_example}''') 139 | 140 | prompt = prompt_container.text_area(label='세부지침 입력', 141 | placeholder='지침을 입력해 주세요', 142 | key='prompt1', 143 | height=250) 144 | 145 | # 미리보기 출력 146 | if prompt: 147 | prompt_output = make_prompt(prompt=prompt, topic=topic, category=category) 148 | 149 | prompt_container.markdown(f'```{prompt_output}') 150 | 151 | # 블로그 생성 152 | if apikey and topic and category and prompt: 153 | button = prompt_container.button('생성하기') 154 | 155 | if button: 156 | filename = generate_blog(apikey=apikey, topic=topic, category=category, prompt=prompt) 157 | download_btn = prompt_container.download_button(label='파일 다운로드', 158 | data=get_file(filename=filename), 159 | file_name=filename, 160 | mime='text/markdown') 161 | 162 | with tab_multiple: 163 | file_upload = st.file_uploader("파일 선택(csv)", type=['csv']) 164 | if file_upload: 165 | df = pd.read_csv(file_upload) 166 | df['topic'] = df.apply(lambda x: x['topic'].replace('<>', x['keyword']), axis=1) 167 | st.dataframe(df) 168 | 169 | # Prompt Container 170 | prompt_container2 = st.container() 171 | prompt_container2.subheader('2. 세부지침') 172 | prompt_container2.markdown('[tip 1] **세부지침**은 [구글 번역기](https://translate.google.com/)로 돌려서 **영어로** 입력해 주세요') 173 | prompt_container2.markdown('[tip 2] `<>`은 입력한 주제로 `<>`는 입력한 카테고리로 **치환**됩니다.') 174 | prompt_container2.markdown('(예시)') 175 | prompt_container2.markdown(f''' 176 | ``` 177 | {prompt_example}''') 178 | 179 | prompt2 = prompt_container2.text_area(label='세부지침 입력', 180 | placeholder='지침을 입력해 주세요', 181 | key='prompt2', 182 | height=250) 183 | 184 | total = len(df) 185 | button2 = prompt_container2.button(f'{total}개 파일 생성하기') 186 | 187 | if button2: 188 | generate_progress = st.progress(0) 189 | st.write(f"[알림] 총{total} 개의 블로그를 생성합니다!") 190 | blog_files = [] 191 | for i, row in df.iterrows(): 192 | filename = generate_blog(apikey=apikey, topic=row['topic'], category=row['category'], prompt=prompt2) 193 | blog_files.append(filename) 194 | st.write(f"[완료] {row['topic']}") 195 | generate_progress.progress((i + 1) / total) 196 | 197 | yesterday = datetime.now() - timedelta(days=1) 198 | timestring = yesterday.strftime('%Y-%m-%d') 199 | zip_filename = f'{timestring}-blog-files.zip' 200 | with zipfile.ZipFile(zip_filename, 'w') as myzip: 201 | for f in blog_files: 202 | myzip.write(f) 203 | myzip.close() 204 | 205 | with open(zip_filename, "rb") as fzip: 206 | download_btn2 = st.download_button(label="파일 다운로드", 207 | data=fzip, 208 | file_name=zip_filename, 209 | mime="application/zip" 210 | ) 211 | 212 | 213 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.3 2 | aiosignal==1.3.1 3 | altair==4.2.0 4 | anyio==3.6.2 5 | appnope==0.1.3 6 | argon2-cffi==21.3.0 7 | argon2-cffi-bindings==21.2.0 8 | arrow==1.2.3 9 | asttokens==2.2.1 10 | async-timeout==4.0.2 11 | attrs==22.2.0 12 | backcall==0.2.0 13 | backports.zoneinfo==0.2.1 14 | beautifulsoup4==4.11.1 15 | bitlyshortener==0.6.4 16 | bleach==5.0.1 17 | blinker==1.5 18 | cachetools==5.2.1 19 | cffi==1.15.1 20 | charset-normalizer==2.1.1 21 | click==8.1.3 22 | comm==0.1.2 23 | commonmark==0.9.1 24 | contourpy==1.0.7 25 | cycler==0.11.0 26 | debugpy==1.6.5 27 | decorator==5.1.1 28 | defusedxml==0.7.1 29 | entrypoints==0.4 30 | et-xmlfile==1.1.0 31 | executing==1.2.0 32 | fastjsonschema==2.16.2 33 | finance-datareader==0.9.50 34 | fonttools==4.38.0 35 | fqdn==1.5.1 36 | frozenlist==1.3.3 37 | gitdb==4.0.10 38 | GitPython==3.1.30 39 | idna==3.4 40 | importlib-metadata==6.0.0 41 | importlib-resources==5.10.2 42 | ipykernel==6.20.2 43 | ipython==8.8.0 44 | ipython-genutils==0.2.0 45 | ipywidgets==8.0.4 46 | isoduration==20.11.0 47 | jedi==0.18.2 48 | Jinja2==3.1.2 49 | jsonpointer==2.3 50 | jsonschema==4.17.3 51 | jupyter==1.0.0 52 | jupyter-console==6.4.4 53 | jupyter-events==0.6.3 54 | jupyter_client==7.4.9 55 | jupyter_core==5.1.3 56 | jupyter_server==2.1.0 57 | jupyter_server_terminals==0.4.4 58 | jupyterlab-pygments==0.2.2 59 | jupyterlab-widgets==3.0.5 60 | kiwisolver==1.4.4 61 | lxml==4.9.2 62 | Markdown==3.4.1 63 | MarkupSafe==2.1.2 64 | matplotlib==3.6.3 65 | matplotlib-inline==0.1.6 66 | mistune==2.0.4 67 | multidict==6.0.4 68 | nbclassic==0.4.8 69 | nbclient==0.7.2 70 | nbconvert==7.2.8 71 | nbformat==5.7.3 72 | nest-asyncio==1.5.6 73 | notebook==6.5.2 74 | notebook_shim==0.2.2 75 | numpy==1.24.1 76 | openai==0.26.4 77 | openpyxl==3.0.10 78 | packaging==23.0 79 | pandas==1.5.3 80 | pandocfilters==1.5.0 81 | parso==0.8.3 82 | pexpect==4.8.0 83 | pickleshare==0.7.5 84 | Pillow==9.4.0 85 | pkgutil_resolve_name==1.3.10 86 | platformdirs==2.6.2 87 | plotly==5.12.0 88 | prometheus-client==0.15.0 89 | prompt-toolkit==3.0.36 90 | protobuf==3.20.3 91 | psutil==5.9.4 92 | ptyprocess==0.7.0 93 | pure-eval==0.2.2 94 | pyarrow==10.0.1 95 | pycparser==2.21 96 | pydeck==0.8.0 97 | Pygments==2.14.0 98 | Pympler==1.0.1 99 | pyparsing==3.0.9 100 | pyrsistent==0.19.3 101 | python-dateutil==2.8.2 102 | python-json-logger==2.0.4 103 | pytz==2022.7.1 104 | pytz-deprecation-shim==0.1.0.post0 105 | PyYAML==6.0 106 | pyzmq==25.0.0 107 | qtconsole==5.4.0 108 | QtPy==2.3.0 109 | requests==2.28.2 110 | requests-file==1.5.1 111 | rfc3339-validator==0.1.4 112 | rfc3986-validator==0.1.1 113 | rich==13.1.0 114 | seaborn==0.12.2 115 | semver==2.13.0 116 | Send2Trash==1.8.0 117 | six==1.16.0 118 | smmap==5.0.0 119 | sniffio==1.3.0 120 | soupsieve==2.3.2.post1 121 | stack-data==0.6.2 122 | streamlit==1.17.0 123 | tenacity==8.1.0 124 | terminado==0.17.1 125 | tinycss2==1.2.1 126 | toml==0.10.2 127 | toolz==0.12.0 128 | tornado==6.2 129 | tqdm==4.64.1 130 | traitlets==5.8.1 131 | typing_extensions==4.4.0 132 | tzdata==2022.7 133 | tzlocal==4.2 134 | uri-template==1.2.0 135 | urllib3==1.26.14 136 | validators==0.20.0 137 | watchdog==2.2.1 138 | wcwidth==0.2.6 139 | webcolors==1.12 140 | webencodings==0.5.1 141 | websocket-client==1.4.2 142 | widgetsnbextension==4.0.5 143 | yarl==1.8.2 144 | zipp==3.11.0 145 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.16 2 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | mkdir -p ~/.streamlit/ 2 | 3 | echo "\ 4 | [server]\n\ 5 | port = $PORT\n\ 6 | enableCORS = false\n\ 7 | headless = true\n\ 8 | \n\ 9 | " > ~/.streamlit/config.toml --------------------------------------------------------------------------------