From 261b2a785b83009b8b477128b093065830b8cb79 Mon Sep 17 00:00:00 2001 From: sanj <67624670+iodrift@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:28:02 -0700 Subject: [PATCH] Auto-update: Tue Jun 25 19:28:02 PDT 2024 --- Extras/webclipper/README.md | 3 + Extras/webclipper/archivist.js | 41 +++++++++ sijapi/classes.py | 1 + sijapi/config/config.py | 98 ---------------------- sijapi/config/email.yaml-example | 2 + sijapi/config/named-locations.yaml-example | 4 + sijapi/config/sd-example.json | 43 ---------- sijapi/config/sd.json | 43 ---------- sijapi/routers/email.py | 88 +++++++++---------- 9 files changed, 92 insertions(+), 231 deletions(-) create mode 100644 Extras/webclipper/README.md create mode 100644 Extras/webclipper/archivist.js delete mode 100644 sijapi/config/config.py create mode 100644 sijapi/config/named-locations.yaml-example delete mode 100644 sijapi/config/sd-example.json delete mode 100644 sijapi/config/sd.json diff --git a/Extras/webclipper/README.md b/Extras/webclipper/README.md new file mode 100644 index 0000000..4f64498 --- /dev/null +++ b/Extras/webclipper/README.md @@ -0,0 +1,3 @@ +This is designed to work with UserScripts and similar browser extensions. Fill in the domain/URL where your sijapi instance is exposed (http://localhost:4444 is fine for the same device, but consider using a reverse proxy to extend to your mobile devices). + +And fill in your GLOBAL_API_KEY that you chose when configuring sijapi. diff --git a/Extras/webclipper/archivist.js b/Extras/webclipper/archivist.js new file mode 100644 index 0000000..64ac8ca --- /dev/null +++ b/Extras/webclipper/archivist.js @@ -0,0 +1,41 @@ +// ==UserScript== +// @name Archivist +// @version 0.1 +// @description archivist userscript posts to sij.ai/clip +// @author sij.ai +// @match *://*/* +// @grant GM_xmlhttpRequest +// ==/UserScript== + +(function() { + 'use strict'; + + window.addEventListener('load', function() { + setTimeout(function() { + var data = new URLSearchParams({ + title: document.title, + url: window.location.href, + referrer: document.referrer || '', + width: window.innerWidth ? window.innerWidth.toString() : '', + encoding: document.characterSet, + source: document.documentElement.outerHTML + }); + + GM_xmlhttpRequest({ + method: 'POST', + url: 'https://!{!{ YOUR DOMAIN HERE }!}!/clip?api_key=!{!{ YOUR API KEY HERE }!}!', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'bearer !{!{ GLOBAL_API_KEY HERE }!}!' + }, + data: data.toString(), + onload: function(response) { + console.log('Data sent to server'); + }, + onerror: function(error) { + console.error('Error sending data:', error); + } + }); + }, 5000); + }); +})(); diff --git a/sijapi/classes.py b/sijapi/classes.py index 809c8f8..e572c6c 100644 --- a/sijapi/classes.py +++ b/sijapi/classes.py @@ -65,6 +65,7 @@ class AutoResponder(BaseModel): name: str style: str context: str + ollama_model: str = "llama3" whitelist: List[str] blacklist: List[str] image_prompt: Optional[str] = None diff --git a/sijapi/config/config.py b/sijapi/config/config.py deleted file mode 100644 index 6fc0b72..0000000 --- a/sijapi/config/config.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import yaml -from time import sleep -from pathlib import Path -import ipaddress - -import yaml - -class Config: - def __init__(self, yaml_file): - with open(yaml_file, 'r') as file: - self.data = yaml.safe_load(file) - - def __getattr__(self, name): - if name in self.data: - value = self.data[name] - if isinstance(value, dict): - return ConfigSection(value) - return value - raise AttributeError(f"Config has no attribute '{name}'") - -class ConfigSection: - def __init__(self, data): - self.data = data - - def __getattr__(self, name): - if name in self.data: - value = self.data[name] - if isinstance(value, dict): - return ConfigSection(value) - return value - raise AttributeError(f"ConfigSection has no attribute '{name}'") - - def __setattr__(self, name, value): - if name == 'data': - super().__setattr__(name, value) - else: - self.data[name] = value - -# Load the YAML configuration file -CFG = Config('.config.yaml') - -# Access existing attributes -print(CFG.API.PORT) # Output: localhost - -def load_config(): - yaml_file = os.path.join(os.path.dirname(__file__), ".config.yaml") - - HOME_DIR = Path.home() - BASE_DIR = Path(__file__).resolve().parent.parent - CONFIG_DIR = BASE_DIR / "config" - ROUTER_DIR = BASE_DIR / "routers" - - DATA_DIR = BASE_DIR / "data" - os.makedirs(DATA_DIR, exist_ok=True) - - ALERTS_DIR = DATA_DIR / "alerts" - os.makedirs(ALERTS_DIR, exist_ok=True) - - LOGS_DIR = BASE_DIR / "logs" - os.makedirs(LOGS_DIR, exist_ok=True) - REQUESTS_DIR = LOGS_DIR / "requests" - os.makedirs(REQUESTS_DIR, exist_ok=True) - REQUESTS_LOG_PATH = LOGS_DIR / "requests.log" - DOC_DIR = DATA_DIR / "docs" - os.makedirs(DOC_DIR, exist_ok=True) - SD_IMAGE_DIR = DATA_DIR / "sd" / "images" - os.makedirs(SD_IMAGE_DIR, exist_ok=True) - SD_WORKFLOWS_DIR = DATA_DIR / "sd" / "workflows" - - - - try: - with open(yaml_file, 'r') as file: - config_data = yaml.safe_load(file) - - vars = { - - - "API": { - - } - } - - - config = Config(config_data) - return config - except Exception as e: - print(f"Error while loading configuration: {e}") - return None - -def reload_config(): - while True: - global config - with open('config.yaml', 'r') as file: - config_data = yaml.safe_load(file) - config = Config(config_data) - sleep(300) # reload every 5 minutes \ No newline at end of file diff --git a/sijapi/config/email.yaml-example b/sijapi/config/email.yaml-example index c4ad15f..31e16ac 100644 --- a/sijapi/config/email.yaml-example +++ b/sijapi/config/email.yaml-example @@ -18,6 +18,7 @@ accounts: - name: work style: professional context: he is currently on leave and will return in late July + ollama_model: llama3 whitelist: - '@work.org' blacklist: @@ -56,6 +57,7 @@ accounts: autoresponders: - name: ai style: cryptic + ollama_model: llama3 context: respond to any inquiries with cryptic and vaguely menacing riddles, esoteric assertions, or obscure references. image_prompt: using visually evocative words, phrases, and sentence fragments, describe an image inspired by the following prompt whitelist: diff --git a/sijapi/config/named-locations.yaml-example b/sijapi/config/named-locations.yaml-example new file mode 100644 index 0000000..5dd3f15 --- /dev/null +++ b/sijapi/config/named-locations.yaml-example @@ -0,0 +1,4 @@ +- name: Echo Valley Ranch + latitude: 42.8098216 + longitude: -123.049396 + radius: 1.5 \ No newline at end of file diff --git a/sijapi/config/sd-example.json b/sijapi/config/sd-example.json deleted file mode 100644 index 3361f1c..0000000 --- a/sijapi/config/sd-example.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "scenes": [ - { - "scene": "default", - "triggers": [""], - "API_PPrompt": "(Highly-detailed) image of ", - "API_SPrompt": "; ((masterpiece)); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", - "API_NPrompt": "`oil, paint splash, oil effect, dots, paint, freckles, liquid effect, canvas frame, 3d, bad art, asian, illustrated, deformed, blurry, duplicate, bad art, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, (easynegative:0.5), epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, explicit, topless`", - "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic images. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words.", - "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this scene description to its essence, staying true to what it describes: ", - "workflows": [{"workflow": "turbo.json", "size": "1024x768"}] - }, - { - "scene": "portrait", - "triggers": [ - "portrait", - "profile", - "headshot" - ], - "API_PPrompt": "Highly-detailed portrait photo of ", - "API_SPrompt": "; attractive, cute, (((masterpiece))); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", - "API_NPrompt": "canvas frame, 3d, ((bad art)), illustrated, deformed, blurry, duplicate, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, (easynegative:0.5), epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, nude", - "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic portrait photos. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided, focusing in particular on the pictured individual's eyes, pose, and other distinctive features. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the rest of the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words. Remember that the final product will be a still image, and action verbs are not as helpful as simple descriptions of position, appearance, background, etc.", - "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this portrait photo to its essence: ", - "workflows": [ - { - "workflow": "selfie.json", - "size": "768x1024" - } - ] - }, - { - "scene": "wallpaper", - "triggers": ["wallpaper"], - "API_PPrompt": "Stunning widescreen image of ", - "API_SPrompt": ", masterpiece, (subtle:0.7), (nuanced:0.6), best quality, ultra detailed, ultra high resolution, 8k, (documentary:0.3), cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, (eliot porter:0.6), (frans lanting:0.4), (daniel kordan:0.6), landscapephotography, ultra detailed, earth tones, moody", - "API_NPrompt": "FastNegativeV2, (easynegative:0.5), canvas frame, 3d, ((bad art)), illustrated, deformed, blurry, duplicate, Photoshop, video game, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, (Thomas Kinkade:0.5), sentimental, kitsch, kitschy, twee, commercial, holiday card, modern, futuristic, urban, comic, cartoon, FastNegativeV2, epiCNegative, easynegative, verybadimagenegative_v1.3", - "llm_sys_msg": "You are a helpful AI who assists in generating prompts that will be used to generate highly realistic images. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words.", - "llm_pre_prompt": "Using a series of words or sentence fragments separated by commas, describe a professional landscape photograph of a striking scene of nature. You can select any place on Earth that a young model from the Pacific Northwest is likely to travel to. Focus on describing the content and composition of the image. Only use words and phrases that are visually descriptive. This model is especially fond of wild and rugged places, mountains. She favors dark muted earth tones, dramatic lighting, and interesting juxtapositions between foreground and background, or center of frame and outer frame areas. Avoid cliche situations; instread strive for nuance and originality in composition and environment.", - "workflows": [{"workflow": "landscape.json", "size": "1160x768"}] - } - ] -} diff --git a/sijapi/config/sd.json b/sijapi/config/sd.json deleted file mode 100644 index 215c20a..0000000 --- a/sijapi/config/sd.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "scenes": [ - { - "scene": "default", - "triggers": [""], - "API_PPrompt": "Highly-detailed image of ", - "API_SPrompt": ", masterpiece, subtle, nuanced, best quality, ultra detailed, ultra high resolution, 8k, documentary, american transcendental, cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, eliot porter, frans lanting, daniel kordan, landscape photography, ultra detailed, earth tones, moody", - "API_NPrompt": "3d, bad art, illustrated, deformed, blurry, duplicate, video game, render, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, Thomas Kinkade, sentimental, kitsch, kitschy, twee, commercial, holiday card, comic, cartoon", - "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic images. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words.", - "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this scene description to its essence, staying true to what it describes: ", - "workflows": [{"workflow": "default.json", "size": "1024x768"}] - }, - { - "scene": "wallpaper", - "triggers": ["wallpaper"], - "API_PPrompt": "Stunning widescreen image of ", - "API_SPrompt": ", masterpiece, subtle, nuanced, best quality, ultra detailed, ultra high resolution, 8k, documentary, american transcendental, cinematic, filmic, moody, dynamic lighting, realistic, wallpaper, landscape photography, professional, earthporn, eliot porter, frans lanting, daniel kordan, landscape photography, ultra detailed, earth tones, moody", - "API_NPrompt": "3d, bad art, illustrated, deformed, blurry, duplicate, video game, render, anime, cartoon, fake, tiling, out of frame, bad art, bad anatomy, 3d render, nsfw, worst quality, low quality, text, watermark, Thomas Kinkade, sentimental, kitsch, kitschy, twee, commercial, holiday card, comic, cartoon", - "llm_sys_msg": "You are a helpful AI who assists in generating prompts that will be used to generate highly realistic images. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words.", - "llm_pre_prompt": "Using a series of words or sentence fragments separated by commas, describe a professional landscape photograph of a striking scene of nature. You can select any place on Earth that a young model from the Pacific Northwest is likely to travel to. Focus on describing the content and composition of the image. Only use words and phrases that are visually descriptive. This model is especially fond of wild and rugged places, mountains. She favors dark muted earth tones, dramatic lighting, and interesting juxtapositions between foreground and background, or center of frame and outer frame areas. Avoid cliche situations; instread strive for nuance and originality in composition and environment.", - "workflows": [{"workflow": "wallpaper.json", "size": "1024x640"}] - }, - { - "scene": "portrait", - "triggers": [ - "portrait", - "profile", - "headshot" - ], - "API_PPrompt": "Highly-detailed portrait photo of ", - "API_SPrompt": "; attractive, cute, (((masterpiece))); ((beautiful lighting)), subdued, fine detail, extremely sharp, 8k, insane detail, dynamic lighting, cinematic, best quality, ultra detailed.", - "API_NPrompt": "canvas frame, 3d, bad art, illustrated, deformed, blurry, duplicate, bad anatomy, worst quality, low quality, watermark, FastNegativeV2, easynegative, epiCNegative, easynegative, verybadimagenegative_v1.3, nsfw, nude", - "llm_sys_msg": "You are a helpful AI who assists in refining prompts that will be used to generate highly realistic portrait photos. Upon receiving a prompt, you refine it by simplifying and distilling it to its essence, retaining the most visually evocative and distinct elements from what was provided, focusing in particular on the pictured individual's eyes, pose, and other distinctive features. You may infer some visual details that were not provided in the prompt, so long as they are consistent with the rest of the prompt. Always use the most visually descriptive terms possible, and avoid any vague or abstract concepts. Do not include any words or descriptions based on other senses or emotions. Strive to show rather than tell. Space is limited, so be efficient with your words. Remember that the final product will be a still image, and action verbs are not as helpful as simple descriptions of position, appearance, background, etc.", - "llm_pre_prompt": "Using the most visually descriptive sentence fragments, phrases, and words, distill this portrait photo to its essence: ", - "workflows": [ - { - "workflow": "selfie.json", - "size": "768x1024" - } - ] - } - ] -} diff --git a/sijapi/routers/email.py b/sijapi/routers/email.py index a1179e9..ca27520 100644 --- a/sijapi/routers/email.py +++ b/sijapi/routers/email.py @@ -28,7 +28,7 @@ from sijapi import DEBUG, INFO, WARN, ERR, CRITICAL from sijapi import PODCAST_DIR, DEFAULT_VOICE, EMAIL_CONFIG from sijapi.routers import tts, llm, sd, locate from sijapi.utilities import clean_text, assemble_journal_path, extract_text, prefix_lines -from sijapi.classes import EmailAccount, IMAPConfig, SMTPConfig, IncomingEmail, EmailContact +from sijapi.classes import EmailAccount, IMAPConfig, SMTPConfig, IncomingEmail, EmailContact, AutoResponder email = APIRouter(tags=["private"]) @@ -39,19 +39,19 @@ def load_email_accounts(yaml_path: str) -> List[EmailAccount]: return [EmailAccount(**account) for account in config['accounts']] -def get_account_by_email(email: str) -> Optional[EmailAccount]: +def get_account_by_email(this_email: str) -> Optional[EmailAccount]: email_accounts = load_email_accounts(EMAIL_CONFIG) for account in email_accounts: - if account.imap.username.lower() == email.lower(): + if account.imap.username.lower() == this_email.lower(): return account return None -def get_imap_details(email: str) -> Optional[IMAPConfig]: - account = get_account_by_email(email) +def get_imap_details(this_email: str) -> Optional[IMAPConfig]: + account = get_account_by_email(this_email) return account.imap if account else None -def get_smtp_details(email: str) -> Optional[SMTPConfig]: - account = get_account_by_email(email) +def get_smtp_details(this_email: str) -> Optional[SMTPConfig]: + account = get_account_by_email(this_email) return account.smtp if account else None @@ -75,55 +75,49 @@ def get_smtp_connection(account: EmailAccount): else: return SMTP(account.smtp.host, account.smtp.port) -def get_matching_autoresponders(email: IncomingEmail, account: EmailAccount) -> List[Dict]: - matching_profiles = [] - def matches_list(item: str, email: IncomingEmail) -> bool: + + +def get_matching_autoresponders(this_email: IncomingEmail, account: EmailAccount) -> List[AutoResponder]: + def matches_list(item: str, this_email: IncomingEmail) -> bool: if '@' in item: - return item in email.sender + return item in this_email.sender else: - return item.lower() in email.subject.lower() or item.lower() in email.body.lower() - + return item.lower() in this_email.subject.lower() or item.lower() in this_email.body.lower() + matching_profiles = [] for profile in account.autoresponders: - whitelist_match = not profile.whitelist or any(matches_list(item, email) for item in profile.whitelist) - blacklist_match = any(matches_list(item, email) for item in profile.blacklist) - + whitelist_match = not profile.whitelist or any(matches_list(item, this_email) for item in profile.whitelist) + blacklist_match = any(matches_list(item, this_email) for item in profile.blacklist) if whitelist_match and not blacklist_match: - matching_profiles.append({ - 'USER_FULLNAME': account.fullname, - 'RESPONSE_STYLE': profile.style, - 'AUTORESPONSE_CONTEXT': profile.context, - 'IMG_GEN_PROMPT': profile.image_prompt, - 'USER_BIO': account.bio - }) - + matching_profiles.append(profile) return matching_profiles -async def generate_auto_response_body(email: IncomingEmail, profile: Dict) -> str: + +async def generate_auto_response_body(this_email: IncomingEmail, profile: AutoResponder, account: EmailAccount) -> str: now = await locate.localize_datetime(dt_datetime.now()) - then = await locate.localize_datetime(email.datetime_received) + then = await locate.localize_datetime(this_email.datetime_received) age = now - then usr_prompt = f''' -Generate a personalized auto-response to the following email: -From: {email.sender} -Sent: {age} ago -Subject: "{email.subject}" -Body: -{email.body} - -Respond on behalf of {profile['USER_FULLNAME']}, who is unable to respond personally because {profile['AUTORESPONSE_CONTEXT']}. -Keep the response {profile['RESPONSE_STYLE']} and to the point, but responsive to the sender's inquiry. -Do not mention or recite this context information in your response. -''' - - sys_prompt = f"You are an AI assistant helping {profile['USER_FULLNAME']} with email responses. {profile['USER_FULLNAME']} is described as: {profile['USER_BIO']}" - + Generate a personalized auto-response to the following email: + From: {this_email.sender} + Sent: {age} ago + Subject: "{this_email.subject}" + Body: + {this_email.body} + Respond on behalf of {account.fullname}, who is unable to respond personally because {profile.context}. + Keep the response {profile.style} and to the point, but responsive to the sender's inquiry. + Do not mention or recite this context information in your response. + ''' + sys_prompt = f"You are an AI assistant helping {account.fullname} with email responses. {account.fullname} is described as: {account.bio}" try: - response = await llm.query_ollama(usr_prompt, sys_prompt, 400) + # async def query_ollama(usr: str, sys: str = LLM_SYS_MSG, model: str = DEFAULT_LLM, max_tokens: int = 200): + response = await llm.query_ollama(usr_prompt, sys_prompt, profile.ollama_model, 400) + DEBUG(f"query_ollama response: {response}") if isinstance(response, str): + response += "\n\n" return response elif isinstance(response, dict): if "message" in response and "content" in response["message"]: @@ -138,7 +132,7 @@ Do not mention or recite this context information in your response. except Exception as e: ERR(f"Error generating auto-response: {str(e)}") - return f"Thank you for your email regarding '{email.subject}'. We are currently experiencing technical difficulties with our auto-response system. We will review your email and respond as soon as possible. We apologize for any inconvenience." + return f"Thank you for your email regarding '{this_email.subject}'. We are currently experiencing technical difficulties with our auto-response system. We will review your email and respond as soon as possible. We apologize for any inconvenience." def clean_email_content(html_content): @@ -252,7 +246,7 @@ tags: markdown_content += f''' --- -{email.body} +{this_email.body} ''' with open(md_path, 'w', encoding='utf-8') as md_file: @@ -269,9 +263,9 @@ tags: async def autorespond(this_email: IncomingEmail, account: EmailAccount): matching_profiles = get_matching_autoresponders(this_email, account) for profile in matching_profiles: - DEBUG(f"Auto-responding to {this_email.subject} with profile: {profile['USER_FULLNAME']}") + DEBUG(f"Auto-responding to {this_email.subject} with profile: {profile.name}") auto_response_subject = f"Auto-Response Re: {this_email.subject}" - auto_response_body = await generate_auto_response_body(this_email, profile) + auto_response_body = await generate_auto_response_body(this_email, profile, account) DEBUG(f"Auto-response: {auto_response_body}") await send_auto_response(this_email.sender, auto_response_subject, auto_response_body, profile, account) @@ -284,8 +278,8 @@ async def send_auto_response(to_email, subject, body, profile, account): message['Subject'] = subject message.attach(MIMEText(body, 'plain')) - if profile['IMG_GEN_PROMPT']: - jpg_path = await sd.workflow(profile['IMG_GEN_PROMPT'], earlyout=False, downscale_to_fit=True) + if profile.image_prompt: + jpg_path = await sd.workflow(profile.image_prompt, earlyout=False, downscale_to_fit=True) if jpg_path and os.path.exists(jpg_path): with open(jpg_path, 'rb') as img_file: img = MIMEImage(img_file.read(), name=os.path.basename(jpg_path))