Spaces:
Runtime error
Runtime error
Andrew Moffat
commited on
Commit
·
88aba82
1
Parent(s):
2d7c6bd
first commit
Browse files- .gitignore +2 -0
- README.md +5 -1
- app.py +146 -0
- gcal.py +121 -0
- google_oauth.py +34 -0
- requirements.txt +2 -0
.gitignore
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
/.venv
|
2 |
+
credentials.json
|
README.md
CHANGED
@@ -9,4 +9,8 @@ app_file: app.py
|
|
9 |
pinned: false
|
10 |
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
9 |
pinned: false
|
10 |
---
|
11 |
|
12 |
+
Demonstrates the following:
|
13 |
+
|
14 |
+
- Connecting to the Google APIs via OAuth2
|
15 |
+
- Generating a DALL-E image
|
16 |
+
- Basic submission form
|
app.py
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import date
|
2 |
+
from typing import cast
|
3 |
+
|
4 |
+
import openai
|
5 |
+
import streamlit as st
|
6 |
+
|
7 |
+
import gcal
|
8 |
+
import google_oauth
|
9 |
+
|
10 |
+
st.title(":calendar: Creative Calendar")
|
11 |
+
st.subheader("Illustrate a month from your Google calendar")
|
12 |
+
|
13 |
+
|
14 |
+
client: openai.OpenAI | None = None
|
15 |
+
month_date: date | None = None
|
16 |
+
name: str | None = None
|
17 |
+
gender: str | None = None
|
18 |
+
genre: str | None = None
|
19 |
+
submitted = False
|
20 |
+
|
21 |
+
creds = google_oauth.load_creds()
|
22 |
+
|
23 |
+
# no Google credentials? we need to authenticate
|
24 |
+
if creds is None:
|
25 |
+
flow = google_oauth.get_flow()
|
26 |
+
url = google_oauth.get_auth_url(flow)
|
27 |
+
st.link_button("Authenticate with Google", url=url)
|
28 |
+
|
29 |
+
auth_code = st.text_input("Google auth code", type="password")
|
30 |
+
if auth_code:
|
31 |
+
creds = google_oauth.get_creds(flow, auth_code)
|
32 |
+
st.rerun()
|
33 |
+
|
34 |
+
# we have Google credentials, make sure we have the other things
|
35 |
+
else:
|
36 |
+
st.checkbox("Authenticated with Google", value=True, disabled=True)
|
37 |
+
openapi_key = st.text_input("OpenAI API key", type="password")
|
38 |
+
if openapi_key:
|
39 |
+
client = openai.OpenAI(api_key=openapi_key)
|
40 |
+
|
41 |
+
with st.form("my_form"):
|
42 |
+
name = st.text_input("Your name")
|
43 |
+
gender = st.radio("Your gender", ["Male", "Female"]) or "Male"
|
44 |
+
genre = st.text_input("Illustration genre", value="fantasy")
|
45 |
+
month_date = cast(date, st.date_input("Select month"))
|
46 |
+
submitted = st.form_submit_button("Submit")
|
47 |
+
|
48 |
+
|
49 |
+
def compose_story_prompt(
|
50 |
+
*,
|
51 |
+
name: str,
|
52 |
+
gender: str,
|
53 |
+
events: list[gcal.Event],
|
54 |
+
genre: str,
|
55 |
+
) -> str:
|
56 |
+
prompt_events = []
|
57 |
+
for ev in events:
|
58 |
+
dt = ev.start.strftime("%B %d, %Y")
|
59 |
+
line = f"{dt}: {ev.title}"
|
60 |
+
if ev.description:
|
61 |
+
line += f" ({ev.description})"
|
62 |
+
prompt_events.append(line)
|
63 |
+
|
64 |
+
event_str = "\n".join(["-" + ev for ev in prompt_events])
|
65 |
+
prompt = f"""Consider the following events from my Google calendar:
|
66 |
+
|
67 |
+
{event_str}
|
68 |
+
|
69 |
+
Write a description for a short {genre} story that captures the major events
|
70 |
+
from my calendar. The description should cover only the major themes from my
|
71 |
+
calendar. Do not write the story itself, just a description of the story. Do
|
72 |
+
not include any references to a calendar or specific dates, just the major
|
73 |
+
themes from my calendar.
|
74 |
+
|
75 |
+
The main character is a {gender} named {name}.
|
76 |
+
"""
|
77 |
+
return prompt
|
78 |
+
|
79 |
+
|
80 |
+
def compose_picture_prompt(story: str, genre: str) -> str:
|
81 |
+
prompt = f"""For the following short {genre} story description, create a single
|
82 |
+
{genre}-style image that captures the major events in the story. Do not
|
83 |
+
include any text in the illustration.
|
84 |
+
-----
|
85 |
+
|
86 |
+
{story}
|
87 |
+
"""
|
88 |
+
return prompt
|
89 |
+
|
90 |
+
|
91 |
+
@st.cache_data(show_spinner="Writing the story...")
|
92 |
+
def write_story(story_prompt):
|
93 |
+
completion = client.chat.completions.create(
|
94 |
+
model="gpt-4",
|
95 |
+
messages=[
|
96 |
+
{
|
97 |
+
"role": "user",
|
98 |
+
"content": [
|
99 |
+
{"type": "text", "text": story_prompt},
|
100 |
+
],
|
101 |
+
}
|
102 |
+
],
|
103 |
+
)
|
104 |
+
content = completion.choices[0].message.content
|
105 |
+
return content
|
106 |
+
|
107 |
+
|
108 |
+
@st.cache_data(show_spinner="Illustrating the cover...")
|
109 |
+
def draw_picture(picture_prompt: str) -> str:
|
110 |
+
response = client.images.generate(
|
111 |
+
model="dall-e-3",
|
112 |
+
prompt=picture_prompt,
|
113 |
+
size="1024x1024",
|
114 |
+
quality="standard",
|
115 |
+
n=1,
|
116 |
+
)
|
117 |
+
image_url = response.data[0].url
|
118 |
+
return cast(str, image_url)
|
119 |
+
|
120 |
+
|
121 |
+
if client and creds and submitted:
|
122 |
+
cal_service = gcal.build_calendar_api(creds)
|
123 |
+
calendars = gcal.get_calendars(cal_service)
|
124 |
+
|
125 |
+
month_start, month_end = gcal.get_start_and_end(cast(date, month_date))
|
126 |
+
events = gcal.get_events(cal_service, month_start, month_end, calendars)
|
127 |
+
|
128 |
+
st.write(
|
129 |
+
f":white_check_mark: Fetched {len(events)} events from "
|
130 |
+
f"{len(calendars)} calendars"
|
131 |
+
)
|
132 |
+
|
133 |
+
story_prompt = compose_story_prompt(
|
134 |
+
name=cast(str, name),
|
135 |
+
gender=cast(str, gender),
|
136 |
+
events=events,
|
137 |
+
genre=cast(str, genre),
|
138 |
+
)
|
139 |
+
story = write_story(story_prompt)
|
140 |
+
|
141 |
+
with st.expander("See the story"):
|
142 |
+
st.write(story)
|
143 |
+
|
144 |
+
picture_prompt = compose_picture_prompt(story, genre)
|
145 |
+
image_url = draw_picture(picture_prompt)
|
146 |
+
st.image(image_url)
|
gcal.py
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import calendar
|
2 |
+
from dataclasses import dataclass
|
3 |
+
from datetime import date, datetime
|
4 |
+
from typing import Generator
|
5 |
+
|
6 |
+
import pytz
|
7 |
+
import streamlit as st
|
8 |
+
from googleapiclient.discovery import build
|
9 |
+
|
10 |
+
|
11 |
+
def build_calendar_api(creds):
|
12 |
+
return build("calendar", "v3", credentials=creds)
|
13 |
+
|
14 |
+
|
15 |
+
@dataclass
|
16 |
+
class Event:
|
17 |
+
id: str
|
18 |
+
title: str
|
19 |
+
description: str | None
|
20 |
+
start: datetime
|
21 |
+
|
22 |
+
|
23 |
+
@st.cache_data(ttl=60, show_spinner="Fetching calendars...")
|
24 |
+
def get_calendars(_service):
|
25 |
+
calendars = []
|
26 |
+
page_token = None
|
27 |
+
while True:
|
28 |
+
calendar_list = _service.calendarList().list(pageToken=page_token).execute()
|
29 |
+
for entry in calendar_list["items"]:
|
30 |
+
cal_id = entry["id"]
|
31 |
+
calendars.append(cal_id)
|
32 |
+
page_token = calendar_list.get("nextPageToken")
|
33 |
+
if not page_token:
|
34 |
+
break
|
35 |
+
|
36 |
+
return calendars
|
37 |
+
|
38 |
+
|
39 |
+
def localize(date_input: datetime | date) -> datetime:
|
40 |
+
local_timezone = pytz.timezone("America/Los_Angeles")
|
41 |
+
timezone_aware_datetime = local_timezone.localize(
|
42 |
+
datetime(date_input.year, date_input.month, date_input.day)
|
43 |
+
)
|
44 |
+
utc_datetime = timezone_aware_datetime.astimezone(pytz.utc)
|
45 |
+
return utc_datetime
|
46 |
+
|
47 |
+
|
48 |
+
@st.cache_data(ttl=60, show_spinner="Fetching calendar events...")
|
49 |
+
def get_events(
|
50 |
+
_service,
|
51 |
+
start: datetime | date,
|
52 |
+
end: datetime | date,
|
53 |
+
calendar_ids: list[str],
|
54 |
+
) -> list[Event]:
|
55 |
+
all_events: list[Event] = []
|
56 |
+
for cal_id in calendar_ids:
|
57 |
+
all_events.extend(
|
58 |
+
_gen_events(
|
59 |
+
_service,
|
60 |
+
calendarId=cal_id,
|
61 |
+
timeMin=localize(start).isoformat(),
|
62 |
+
timeMax=localize(end).isoformat(),
|
63 |
+
singleEvents=True,
|
64 |
+
orderBy="startTime",
|
65 |
+
)
|
66 |
+
)
|
67 |
+
all_events.sort(key=lambda ev: ev.start)
|
68 |
+
return all_events
|
69 |
+
|
70 |
+
|
71 |
+
def _gen_events(_service, **kwargs) -> Generator[Event, None, None]:
|
72 |
+
page_token = None
|
73 |
+
while True:
|
74 |
+
events = (
|
75 |
+
_service.events()
|
76 |
+
.list(
|
77 |
+
**kwargs,
|
78 |
+
pageToken=page_token,
|
79 |
+
)
|
80 |
+
.execute()
|
81 |
+
)
|
82 |
+
for event in events["items"]:
|
83 |
+
start = _parse_date(event["start"])
|
84 |
+
ev = Event(
|
85 |
+
id=event["id"],
|
86 |
+
title=event["summary"],
|
87 |
+
description=event.get("description"),
|
88 |
+
start=start,
|
89 |
+
)
|
90 |
+
yield ev
|
91 |
+
|
92 |
+
page_token = events.get("nextPageToken")
|
93 |
+
if not page_token:
|
94 |
+
break
|
95 |
+
|
96 |
+
|
97 |
+
def _parse_date(date_obj: dict) -> datetime:
|
98 |
+
if "dateTime" in date_obj:
|
99 |
+
dt = datetime.fromisoformat(date_obj["dateTime"].rstrip("Z"))
|
100 |
+
|
101 |
+
elif "date" in date_obj:
|
102 |
+
dt = datetime.fromisoformat(date_obj["date"])
|
103 |
+
else:
|
104 |
+
raise ValueError("Event has no start date")
|
105 |
+
|
106 |
+
is_naive = dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None
|
107 |
+
if is_naive:
|
108 |
+
if "timeZone" in date_obj:
|
109 |
+
timezone = pytz.timezone(date_obj["timeZone"])
|
110 |
+
dt = timezone.localize(dt)
|
111 |
+
else:
|
112 |
+
dt = pytz.utc.localize(dt)
|
113 |
+
|
114 |
+
return dt
|
115 |
+
|
116 |
+
|
117 |
+
def get_start_and_end(month_date: date) -> tuple[date, date]:
|
118 |
+
start_of_month = date(month_date.year, month_date.month, 1)
|
119 |
+
last_day_of_month = calendar.monthrange(month_date.year, month_date.month)[1]
|
120 |
+
end_of_month = date(month_date.year, month_date.month, last_day_of_month)
|
121 |
+
return start_of_month, end_of_month
|
google_oauth.py
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from google_auth_oauthlib.flow import Flow
|
3 |
+
|
4 |
+
SCOPES = [
|
5 |
+
"https://www.googleapis.com/auth/calendar.events.readonly",
|
6 |
+
"https://www.googleapis.com/auth/calendar.readonly",
|
7 |
+
]
|
8 |
+
|
9 |
+
|
10 |
+
@st.cache_resource
|
11 |
+
def get_flow():
|
12 |
+
flow = Flow.from_client_secrets_file(
|
13 |
+
"credentials.json",
|
14 |
+
scopes=SCOPES,
|
15 |
+
redirect_uri="urn:ietf:wg:oauth:2.0:oob",
|
16 |
+
)
|
17 |
+
return flow
|
18 |
+
|
19 |
+
|
20 |
+
def get_auth_url(flow: Flow) -> str:
|
21 |
+
auth_url, _ = flow.authorization_url(prompt="consent")
|
22 |
+
return auth_url
|
23 |
+
|
24 |
+
|
25 |
+
@st.cache_resource
|
26 |
+
def get_creds(_flow: Flow, code: str):
|
27 |
+
_flow.fetch_token(code=code)
|
28 |
+
creds = _flow.credentials
|
29 |
+
st.session_state.creds = creds
|
30 |
+
return creds
|
31 |
+
|
32 |
+
|
33 |
+
def load_creds() -> str | None:
|
34 |
+
return st.session_state.get("creds", None)
|
requirements.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
google-auth-oauthlib
|
2 |
+
google-api-python-client
|