Andrew Moffat commited on
Commit
88aba82
·
1 Parent(s): 2d7c6bd

first commit

Browse files
Files changed (6) hide show
  1. .gitignore +2 -0
  2. README.md +5 -1
  3. app.py +146 -0
  4. gcal.py +121 -0
  5. google_oauth.py +34 -0
  6. 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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
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