diff --git a/alwrity.py b/alwrity.py index 62bfab2e..293ceabb 100644 --- a/alwrity.py +++ b/alwrity.py @@ -307,6 +307,7 @@ def main(): Welcome to Alwrity! """, unsafe_allow_html=True) + # Export the paths and file names. Dont want alwrity to be chatty and prompt for inputs. os.environ["SEARCH_SAVE_FILE"] = os.path.join(os.getcwd(), "lib", "workspace", "alwrity_web_research", f"web_research_report_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}") @@ -375,7 +376,7 @@ def write_blog(): "AI Copywriter", "Quit" ] - choice = st.selectbox("**Select a content creation type:**", options, index=0, format_func=lambda x: f"๐Ÿ“ {x}") + choice = st.selectbox("**๐Ÿ‘‡Select a content creation type:**", options, index=0, format_func=lambda x: f"๐Ÿ“ {x}") if choice == "AI Blog Writer": blog_from_keyword() diff --git a/lib/ai_writers/image_ai_writer.py b/lib/ai_writers/image_ai_writer.py new file mode 100644 index 00000000..78c9657f --- /dev/null +++ b/lib/ai_writers/image_ai_writer.py @@ -0,0 +1,112 @@ +import sys +import os + +from textwrap import dedent +from PIL import Image +import json +from pathlib import Path +from datetime import datetime +import streamlit as st + +from dotenv import load_dotenv +load_dotenv(Path('../../.env')) +from loguru import logger +logger.remove() +logger.add(sys.stdout, + colorize=True, + format="{level}|{file}:{line}:{function}| {message}" + ) + +from ..ai_web_researcher.firecrawl_web_crawler import scrape_url +from ..blog_metadata.get_blog_metadata import blog_metadata +from ..blog_postprocessing.save_blog_to_file import save_blog_to_file +from ..gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image +from ..gpt_providers.text_generation.main_text_generation import llm_text_gen + +import google.generativeai as genai + + +def blog_from_image(prompt, uploaded_img): + """ + This function will take a blog Topic to first generate sections for it + and then generate content for each section. + """ + # Use to store the blog in a string, to save in a *.md file. + blog_markdown_str = None + logger.info(f"Researching and Writing Blog on {uploaded_img} and {prompt}") + # FIXME: Implement support for Openai. + if not os.getenv("GEMINI_API_KEY"): + st.error("Only Gemini supported, Open Issue ticket on github for Openai, others.") + st.stop() + + with st.status("Started Writing from Image..", expanded=True) as status: + st.empty() + status.update(label=f"Researching and Writing Blog on given Image") + try: + blog_markdown_str = write_blog_from_image(prompt, uploaded_img) + except Exception as err: + st.error(f"Failed to write blog from Image - Error: {err}") + logger.error(f"Failed to write blog from image: {err}") + st.stop() + status.update(label="Successfully wrote blog from image.", expanded=False, state="complete") + + try: + status.update(label="๐Ÿ™Ž Generating - Title, Meta Description, Tags, Categories for the content.") + blog_title, blog_meta_desc, blog_tags, blog_categories = blog_metadata(blog_markdown_str) + except Exception as err: + st.error(f"Failed to get blog metadata: {err}") + + try: + status.update(label="๐Ÿ™Ž Generating Image for the new blog.") + generated_image_filepath = generate_image(f"{blog_title} + ' ' + {blog_meta_desc}") + except Exception as err: + st.warning(f"Failed in Image generation: {err}") + + saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc, + blog_tags, blog_categories, generated_image_filepath) + status.update(label=f"Saved the content in this file: {saved_blog_to_file}") + logger.info(f"\n\n --------- Finished writing Blog -------------- \n") + st.image(generated_image_filepath, caption=blog_title) + st.markdown(f"{blog_markdown_str}") + status.update(label=f"Finished, Review & Use your Original Content Below: {saved_blog_to_file}", state="complete") + + # Clean up the temporary file after processing (optional) + os.remove(uploaded_img) + + +def write_blog_from_image(prompt, uploaded_img): + """Combine the given online research and GPT blog content""" + try: + config_path = Path(os.environ["ALWRITY_CONFIG"]) + with open(config_path, 'r', encoding='utf-8') as file: + config = json.load(file) + except Exception as err: + logger.error(f"Error: Failed to read values from config: {err}") + exit(1) + + blog_characteristics = config['Blog Content Characteristics'] + + if not prompt: + prompt = f""" + As expert Creative Content writer, analyse the given image carefully. + I want you to write a detailed {blog_characteristics['Blog Type']} blog post including 5 FAQs. + + Below are the guidelines to follow: + 1). You must respond in {blog_characteristics['Blog Language']} language. + 2). Tone and Brand Alignment: Adjust your tone, voice, personality for {blog_characteristics['Blog Tone']} audience. + 3). Make sure your response content length is of {blog_characteristics['Blog Length']} words. + """ + logger.info("Generating blog and FAQs from Google web search results.") + + try: + #response = llm_text_gen(prompt) + genai.configure(api_key=os.getenv('GEMINI_API_KEY')) + version = 'models/gemini-1.5-flash' + model = genai.GenerativeModel(version) + model_info = genai.get_model(version) + print(f'{version} - input limit: {model_info.input_token_limit}, output limit: {model_info.output_token_limit}') + response = model.generate_content([prompt, Image.open(uploaded_img)]) + return response.text + except Exception as err: + logger.error(f"Exit: Failed to get response from LLM: {err}") + exit(1) diff --git a/lib/ai_writers/web_url_ai_writer.py b/lib/ai_writers/web_url_ai_writer.py index 80df2795..190b2e51 100644 --- a/lib/ai_writers/web_url_ai_writer.py +++ b/lib/ai_writers/web_url_ai_writer.py @@ -71,19 +71,10 @@ def blog_from_url(weburl): saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath) status.update(label=f"Saved the content in this file: {saved_blog_to_file}") - blog_frontmatter = dedent(f""" - \n--------------------------------------------------------------------- - title: {blog_title}\n - categories: [{blog_categories}]\n - tags: [{blog_tags}]\n - Meta description: {blog_meta_desc.replace(":", "-")}\n - ---------------------------------------------------------------------\n - """) logger.info(f"\n\n --------- Finished writing Blog for : {weburl} -------------- \n") - st.markdown(f"{blog_frontmatter}") st.image(generated_image_filepath) st.markdown(f"{blog_markdown_str}") - status.update(label=f"Finished, Review & Use your Original Content Below: {saved_blog_to_file}") + status.update(label=f"Finished, Review & Use your Original Content Below: {saved_blog_to_file}", state="complete") def write_blog_from_weburl(scraped_website): diff --git a/lib/gpt_providers/text_generation/gemini_pro_text.py b/lib/gpt_providers/text_generation/gemini_pro_text.py index a2d55244..347227d9 100644 --- a/lib/gpt_providers/text_generation/gemini_pro_text.py +++ b/lib/gpt_providers/text_generation/gemini_pro_text.py @@ -29,7 +29,7 @@ def gemini_text_response(prompt, temperature, top_p, n, max_tokens): except Exception as err: logger.error(f"Failed to configure Gemini: {err}") logger.info(f"Temp: {temperature}, MaxTokens: {max_tokens}, TopP: {top_p}, N: {n}") - # Set up the model + # Set up AI model config generation_config = { "temperature": temperature, "top_p": top_p, diff --git a/lib/utils/alwrity_utils.py b/lib/utils/alwrity_utils.py index 40c248fd..512e588a 100644 --- a/lib/utils/alwrity_utils.py +++ b/lib/utils/alwrity_utils.py @@ -1,6 +1,7 @@ import os import re import streamlit as st +import tempfile from pathlib import Path import configparser from datetime import datetime @@ -21,6 +22,7 @@ from lib.ai_writers.twitter_ai_writer import tweet_writer from lib.ai_writers.insta_ai_writer import insta_writer from lib.ai_writers.youtube_ai_writer import write_yt_title, write_yt_description, write_yt_script from lib.ai_writers.web_url_ai_writer import blog_from_url +from lib.ai_writers.image_ai_writer import blog_from_image from lib.ai_writers.ai_story_writer import ai_story_generator from lib.ai_writers.ai_essay_writer import ai_essay_generator from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image @@ -61,11 +63,15 @@ def process_input(input_text, uploaded_file): st.write("PDF file uploaded. Add your PDF processing logic here.") elif uploaded_file.type in ["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/msword"]: st.write("Word document uploaded. Add your DOCX processing logic here.") + elif uploaded_file.type.startswith("image/"): st.image(uploaded_file) + return("image_file") + elif uploaded_file.type.startswith("audio/"): st.audio(uploaded_file) return("audio_file") + elif uploaded_file.type.startswith("video/"): st.video(uploaded_file) @@ -75,7 +81,7 @@ def blog_from_keyword(): st.title("Blog Content Writer") col1, col2 = st.columns([2, 1.5]) with col1: - user_input = st.text_area('**Enter Keywords/Title/YouTube Link/Web URLs**', + user_input = st.text_area('**๐Ÿ‘‡Enter Keywords/Title/YouTube Link/Web URLs**', help='Provide keywords, titles, YouTube links, or web URLs to generate content.', placeholder="""Write Blog From: - Keywords/Blog Title: Provide keywords to web research & write blog. @@ -84,11 +90,19 @@ def blog_from_keyword(): - Web URLs: Provide web URL to write similar blog on.""") with col2: - uploaded_file = st.file_uploader("**Attach files (Audio, Video, Image, Document)**", + uploaded_file = st.file_uploader("**๐Ÿ‘‡Attach files (Audio, Video, Image, Document)**", type=["txt", "pdf", "docx", "jpg", "jpeg", "png", "mp3", "wav", "mp4", "mkv", "avi"], help='Attach files such as audio, video, images, or documents.') - content_type = st.radio("Select content type:", ["Normal-length content", "Long-form content", "Experimental - AI Agents team"]) + temp_file_path = None + if uploaded_file is not None: + # Save the uploaded file to a temporary file + with tempfile.NamedTemporaryFile(delete=False, suffix=uploaded_file.name) as temp_file: + temp_file.write(uploaded_file.read()) + temp_file_path = temp_file.name + + + content_type = st.radio("**๐Ÿ‘‡Select content type:**", ["Normal-length content", "Long-form content", "Experimental - AI Agents team"]) if st.button("Write Blog"): # Clear the previous results from the screen st.empty() @@ -126,6 +140,8 @@ def blog_from_keyword(): generate_audio_blog(user_input) elif 'web_url' in input_type: blog_from_url(user_input) + elif 'image_file' in input_type: + blog_from_image(user_input, temp_file_path) def ai_agents_team(): diff --git a/lib/workspace/alwrity_ui_styling.css b/lib/workspace/alwrity_ui_styling.css index 3f99b309..f3661c59 100644 --- a/lib/workspace/alwrity_ui_styling.css +++ b/lib/workspace/alwrity_ui_styling.css @@ -3,15 +3,13 @@ body { background: #f0f4f8; background-image: linear-gradient(to bottom right, #d0e1f9, #e1ebf9); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - margin: 0; /* Remove any default margin */ - padding: 0; /* Remove any default padding */ + margin: 0; + padding: 0; } + .block-container { - padding-top: 2.5rem; - padding-bottom: 3rem; - padding-left: 5rem; - padding-right: 5rem; - } + padding: 2.5rem 5rem 3rem 5rem; +} /* Main header styling */ .main-header { @@ -21,7 +19,7 @@ body { margin-bottom: 20px; text-align: center; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); - padding-top: 10px; /* Add padding to the top to move it up */ + padding-top: 10px; } /* Sub-header styling */ @@ -36,11 +34,11 @@ body { /* Navigation tabs styling */ .stTabs [role="tab"] { - font-size: 3.75em; /* Increased font size */ + font-size: 1.25em; font-weight: bold; color: white; background: #1565C0; - padding: 16px 24px; /* Increased padding */ + padding: 16px 24px; margin: 5px; border-radius: 8px; border: 2px solid #ddd; @@ -57,11 +55,9 @@ body { border: 2px solid #1565C0; } - - /* Sidebar header styling */ .sidebar-header { - font-size: 2.2em; + font-size: 1.5em; font-weight: bold; color: #333; margin-bottom: 20px; @@ -70,8 +66,14 @@ body { /* Sidebar option styling */ .sidebar-option { margin-bottom: 10px; - font-size: 2.2em; + font-size: 1.1em; color: #1565C0; + cursor: pointer; + transition: color 0.3s ease; +} + +.sidebar-option:hover { + color: #1976D2; } /* Content section styling */ @@ -80,11 +82,11 @@ body { margin-bottom: 30px; border-radius: 10px; box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1); - background-color: #f0f4f8; /* Change background to match body */ + background-color: #ffffff; } /* Input fields styling */ -input[type="text"] { +input[type="text"], input[type="file"], select { width: 100%; padding: 12px; margin: 8px 0; @@ -92,11 +94,18 @@ input[type="text"] { border: 2px solid #ddd; border-radius: 4px; background-color: #f0f8ff; + font-size: 16px; + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +input[type="text"]:focus, input[type="file"]:focus, select:focus { + border-color: #1565C0; + box-shadow: 0 0 5px rgba(21, 101, 192, 0.5); } /* Custom button styling */ div.stButton > button:first-child { - background: #1565C0; /* Match tab color */ + background: #1565C0; color: white; border: none; padding: 12px 24px; @@ -113,7 +122,7 @@ div.stButton > button:first-child { } div.stButton > button:hover:first-child { - background-color: #1976A2; /* Match tab hover color */ + background-color: #1976A2; box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3); } @@ -166,20 +175,47 @@ div.row-widget.stRadio > div[role="radiogroup"] > label[data-baseweb="radio"] { margin-bottom: 10px; color: #333; } + +/* Audio player styling */ audio::-webkit-media-controls-panel, audio::-webkit-media-controls-enclosure { - background-color:#532b5a;} + background-color: #532b5a; +} audio::-webkit-media-controls-time-remaining-display, audio::-webkit-media-controls-current-time-display { color: white; - text-shadow: none; - + text-shadow: none; } audio::-webkit-media-controls-timeline { - background-color: #532b5a; - border-radius: 25px; - margin-left: 10px; - margin-right: 10px; + background-color: #532b5a; + border-radius: 25px; + margin-left: 10px; + margin-right: 10px; } + +/* Select input styling */ +.stSelectbox > div[data-baseweb="select"] > div { + padding: 12px; + margin: 8px 0; + border: 2px solid #ddd; + border-radius: 4px; + background-color: #f0f8ff; + font-size: 16px; + transition: border-color 0.3s ease, box-shadow 0.3s ease; + appearance: none; + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 12px; +} + +select:focus { + border-color: #1565C0; + box-shadow: 0 0 5px rgba(21, 101, 192, 0.5); +} + +select option { + padding: 10px; +} +