File size: 10,759 Bytes
1d6ee52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import streamlit as st
import openai
import base64
import requests
import os
import re
import pandas as pd
import random
import json
import subprocess

# Function to save API key and image directory to a file
def save_settings(api_key, image_directory):
    with open('settings.txt', 'w') as f:
        f.write(f"{api_key}\n{image_directory}")

# Function to load API key and image directory from a file
def load_settings():
    try:
        with open('settings.txt', 'r') as f:
            lines = f.readlines()
            api_key = lines[0].strip() if len(lines) > 0 else ''
            image_directory = lines[1].strip() if len(lines) > 1 else ''
            return api_key, image_directory
    except FileNotFoundError:
        return '', ''

# Function to initialize OpenAI API key
def init_openai_api_key(api_key):
    openai.api_key = api_key

# Function to call GPT API
def call_gpt_api(prompt, temperature=0.7, max_tokens=None):
    params = {
        "model": "gpt-3.5-turbo-0301",
        "messages": [
            {"role": "system", "content": "You are a helpful assistant that generates metadata."},
            {"role": "user", "content": prompt}
        ],
        "temperature": temperature
    }
    if max_tokens is not None:
        params["max_tokens"] = max_tokens

    response = openai.ChatCompletion.create(**params)
    return response.choices[0].message['content']

# Function to call GPT-4o for vision capabilities
def call_gpt_4o_vision(image_path, api_key):
    with open(image_path, 'rb') as image_file:
        image_data = image_file.read()
    image_base64 = base64.b64encode(image_data).decode('utf-8')

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}"
    }

    payload = {
        "model": "gpt-4o",
        "messages": [
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": "Generate a concise name and detailed context for this image, ensuring response fits within 77 tokens. No copyright, No colon (:), trademarks, privacy rights, property rights, no number, no ensuring organization and clarity. No quotation marks or dashes, using commas for separation. Focuses on straightforward, richly descriptive titles without vague language or mentioning camera specifics or photography techniques. Ensure the response is a single line."},
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}}
                ]
            }
        ],
        "max_tokens": 77
    }

    response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
    response_json = response.json()
    if 'choices' in response_json and len(response_json['choices']) > 0:
        return response_json['choices'][0]['message']['content'].replace('\n', ' ')
    else:
        return "No description available"

# Function to clean and process metadata
def clean_metadata(description):
    description = re.split(r'Job ID:|--ar \d+:\d+', description)[0].strip()
    description = description.replace('"', '').replace("'", '')
    return description

# Function to clean numbers and periods from keywords
def clean_numbers_and_periods(keywords):
    cleaned_keywords = []
    for keyword in keywords:
        cleaned_keyword = re.sub(r'\d+|\.', '', keyword).strip()
        if cleaned_keyword:
            cleaned_keywords.append(cleaned_keyword)
    return cleaned_keywords

# Function to generate keywords with retries
def generate_keywords_with_retries(description, keywords_rule, min_keywords=49, max_keywords=49, retries=1):
    keywords_set = set()
    for _ in range(retries):
        keywords_prompt = f"{keywords_rule}\nDescription: {description}"
        keywords = call_gpt_api(keywords_prompt).strip()
        keywords_list = [k.strip() for k in keywords.split(',')]
        keywords_list = clean_numbers_and_periods(keywords_list)
        keywords_set.update(keywords_list)
        if len(keywords_set) >= min_keywords:
            break

    keywords_list = list(keywords_set)
    if len(keywords_list) > max_keywords:
        keywords_list = random.sample(keywords_list, max_keywords)
    elif len(keywords_list) < min_keywords:
        additional_keywords_needed = min_keywords - len(keywords_list)
        for _ in range(retries):
            keywords_prompt = f"{keywords_rule}\nDescription: {description}"
            keywords = call_gpt_api(keywords_prompt).strip()
            keywords_list.extend([k.strip() for k in keywords.split(',') if k.strip() not in keywords_list])
            keywords_list = clean_numbers_and_periods(keywords_list)
            if len(keywords_list) >= min_keywords:
                break
        keywords_list = keywords_list[:max_keywords]

    return ', '.join(keywords_list)

# Function to generate concise names and detailed contexts
def generate_concise_names(description):
    concise_names_rule = (
        "You are a creative assistant. Generate a concise name and detailed context for this image, ensuring response fits within 77 tokens. "
        "No copyright, No colon (:), trademarks, privacy rights, property rights, no number, no ensuring organization and clarity. "
        "No quotation marks or dashes, using commas for separation. Focuses on straightforward, richly descriptive titles without vague language or mentioning camera specifics or photography techniques. "
        "Ensure the response is a single line. if it's too long, make it short and fit."
    )
    concise_names_prompt = f"{concise_names_rule}\nDescription: {description}"
    result = call_gpt_api(concise_names_prompt, temperature=0.7, max_tokens=77).strip()
    result = result.replace('Title:', '').strip()
    result = result.replace('Name:', '').strip()
    result = result.replace('"', '')
    result = result.replace('\n', ' ')
    return result

# Function to generate metadata
def generate_metadata(description, use_concise_names):
    if use_concise_names and not description:
        return None

    keywords_rule = (
        "Generate 40-49 single-word keywords for a microstock image. Ensure diversity by creatively linking "
        "first ten must come from input name, the rest is related concepts (e.g., cross leads to religion, christ). For a black cat, use black, cat, etc. Avoid plurals, and it must be on the same horizontal row"
        "format with commas. Don't generate numbers such as 1. Butterfly 2. Open 3. Pages 4. White"
        "don't use photography techniques. No copyright, No trademarks, "
        "No privacy rights, or property rights."
    )
    title = generate_concise_names(description) if use_concise_names else clean_metadata(description)
    if not title:
        return None
    title = title.replace('\n', ' ')
    keywords = generate_keywords_with_retries(description, keywords_rule)
    metadata = {'title': title, 'keywords': keywords}
    return metadata

def read_image(image_path):
    description = ""
    try:
        if hasattr(sys, '_MEIPASS'):
            exiftool_path = os.path.join(sys._MEIPASS, 'exiftool', 'exiftool.exe')
        else:
            exiftool_path = os.path.join(os.path.dirname(__file__), 'exiftool', 'exiftool.exe')

        result = subprocess.run([exiftool_path, '-j', image_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if result.returncode != 0:
            return None, ""
        metadata_list = json.loads(result.stdout.decode('utf-8'))
        if metadata_list:
            metadata = metadata_list[0]
            description = (metadata.get("Caption-Abstract") or 
                           metadata.get("ImageDescription") or 
                           metadata.get("Description") or 
                           metadata.get("XPComment") or 
                           metadata.get("UserComment") or "")
            if description:
                description = re.split(r'Job ID:|--ar \d+:\d+', description)[0].strip()
    except Exception as e:
        pass
    return None, description

def generate_metadata_for_images(directory, use_repeating_mode, use_concise_names, use_gpt_4o, api_key):
    metadata_list = []
    files = [f for f in os.listdir(directory) if f.lower().endswith(('png', 'jpg', 'jpeg'))]
    
    title_keywords_map = {}

    for filename in files:
        file_path = os.path.join(directory, filename)
        
        if use_gpt_4o:
            description = call_gpt_4o_vision(file_path, api_key)
        else:
            _, description = read_image(file_path)
        if description:
            title = generate_concise_names(description) if use_concise_names else clean_metadata(description)
            if use_repeating_mode and title in title_keywords_map:
                keywords = title_keywords_map[title]
            else:
                metadata = generate_metadata(description, use_concise_names)
                title_keywords_map[title] = metadata['keywords']
                keywords = metadata['keywords']
            metadata_list.append({'Filename': filename, 'Title': title, 'Keywords': keywords})
    return metadata_list

# Streamlit UI
st.title("Metadata Generator by Gasia")
st.write("อย่าลืมเปลี่ยนเป็นภาษาอังกฤษ")

api_key = st.text_input("OpenAI API Key")
image_directory = st.text_input("Images Folder")
use_repeating_mode = st.checkbox("Enable Save Mode", value=True)
use_concise_names = st.checkbox("Generate Concise Names")
use_gpt_4o = st.checkbox("Use GPT-4o Vision")
convert_to_png = st.checkbox("Convert .JPG/.JPEG to .PNG")

if st.button("Submit"):
    if not api_key or not image_directory:
        st.warning("Please fill in all fields")
    else:
        save_settings(api_key, image_directory)
        init_openai_api_key(api_key)

        metadata_list = generate_metadata_for_images(image_directory, use_repeating_mode, use_concise_names, use_gpt_4o, api_key)

        if convert_to_png:
            for metadata in metadata_list:
                if metadata['Filename'].lower().endswith(('.jpg', '.jpeg')):
                    metadata['Filename'] = re.sub(r'\.(jpg|jpeg)$', '.png', metadata['Filename'], flags=re.IGNORECASE)
        
        if metadata_list:
            df = pd.DataFrame(metadata_list)
            st.write(df)
            csv = df.to_csv(index=False).encode('utf-8')
            st.download_button(
                label="Download metadata as CSV",
                data=csv,
                file_name='image_metadata.csv',
                mime='text/csv',
            )

if st.button("Reset Settings"):
    api_key = ''
    image_directory = ''
    save_settings(api_key, image_directory)
    st.success("Settings have been reset to default values")