Auto-Renaming My Untitled.ipynb Files With Gemini 1.5 Flash
I'm starting to accumulate many UntitledX.ipynb files. Here I use the Gemini 1.5 Flash language model from Google to rename each one based on its contents.
from datetime import datetime
from execnb.nbio import read_nb
from fastcore.utils import *
import google.generativeai as genai
from pathlib import Path
Desired File Format
YYYY-MM-DD-Title-of-Notebook-in-TitleCase-With-Hyphens.ipynb
Get the Untitled Notebooks
nbs = L(Path().glob("Untitled*.ipynb"))
nbs
(#32) [Path('Untitled10.ipynb'),Path('Untitled7.ipynb'),Path('Untitled12.ipynb'),Path('Untitled5.ipynb'),Path('Untitled1.ipynb'),Path('Untitled16.ipynb'),Path('Untitled30.ipynb'),Path('Untitled29.ipynb'),Path('Untitled3.ipynb'),Path('Untitled14.ipynb'),Path('Untitled4.ipynb'),Path('Untitled13.ipynb'),Path('Untitled6.ipynb'),Path('Untitled11.ipynb'),Path('Untitled1-Copy1.ipynb'),Path('Untitled15.ipynb'),Path('Untitled2.ipynb'),Path('Untitled28.ipynb'),Path('Untitled17.ipynb'),Path('Untitled26.ipynb')...]
nb = nbs[0]
Get the Last Modified Date
To get each file's prefix, I get the file modified stats:
Path(nb).stat().st_mtime
1736267381.7581108
This returns a Unix timestamp. To get a more readable datetime:
last_modified = datetime.fromtimestamp(Path(nb).stat().st_mtime)
last_modified
datetime.datetime(2025, 1, 7, 16, 29, 41, 758111)
last_modified.strftime('%Y-%m-%d')
'2025-01-07'
Check for an Existing Title
It would be in the first cell:
cells = L(read_nb(nb).cells)
cells[0]
{ 'cell_type': 'markdown',
'id': 'c68e6923',
'idx_': 0,
'metadata': {},
'source': 'git-nbdiffdriver diff: git-nbdiffdriver: command not found'}
Ask Gemini to Title the Notebook
model = genai.GenerativeModel('gemini-1.5-flash-latest')
model
genai.GenerativeModel(
model_name='models/gemini-1.5-flash-latest',
generation_config={},
safety_settings={},
tools=None,
system_instruction=None,
cached_content=None
)
with open(x) as f:
nb = f.read()
def generate_title_part(nb):
prompt = f"""Given this Jupyter notebook, create a filename following these EXACT steps:
1. Extract the title from the first cell if it starts with '#'. In this case it's: "FastHTML By Example, Part 2"
2. Convert to the format: Words-In-Title-Case-With-Hyphens.ipynb
3. Remove any special characters (like commas)
4. If the filename sounds repetitive, simplify it.
5. If the first cell does not contain a title, create one based on the entire notebook's contents.
<notebook>
{nb}
</notebook>
Return ONLY the filename, nothing else."""
safety_settings = [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE",},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE",},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE",},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE",},
]
response = model.generate_content(prompt, safety_settings=safety_settings, request_options = {"timeout": 1000})
try:
return response.text
except Exception as ex:
raise ex
result = generate_title_part(nb)
print(result)
Git-Nbdiffdriver-And-Nbstripout-Issues.ipynb
Prefix the Title With the Date
Putting the full title together:
full_title = f"{last_modified.strftime('%Y-%m-%d')}-{result.strip()}"
print(full_title)
2025-01-07-Git-Nbdiffdriver-And-Nbstripout-Issues.ipynb
Rename the File
x
Path('Untitled10.ipynb')
new_path = Path(full_title)
if new_path.exists():
print(f"Warning: {new_path} already exists")
else:
x.rename(new_path)
print(f"Renamed {x} to {new_path}")
Renamed Untitled10.ipynb to 2025-01-07-Git-Nbdiffdriver-And-Nbstripout-Issues.ipynb
Make This All a Function
Let's put this all together into a function that we can call on several files.
def rename_notebook(nb_path):
"""Rename an untitled notebook based on its contents and modification date"""
date = datetime.fromtimestamp(Path(nb_path).stat().st_mtime).strftime('%Y-%m-%d')
with open(nb_path) as f: nb = f.read()
title_part = generate_title_part(nb)
new_name = f"{date}-{title_part.strip()}"
new_path = Path(new_name)
if new_path.exists():
print(f"Warning: {new_path} already exists")
return nb_path
else:
nb_path.rename(new_path)
print(f"Renamed {nb_path} to {new_path}")
return new_path
nbs = L(Path().glob("Untitled*.ipynb"))
nbs
(#30) [Path('Untitled12.ipynb'),Path('Untitled5.ipynb'),Path('Untitled1.ipynb'),Path('Untitled16.ipynb'),Path('Untitled30.ipynb'),Path('Untitled29.ipynb'),Path('Untitled3.ipynb'),Path('Untitled14.ipynb'),Path('Untitled4.ipynb'),Path('Untitled13.ipynb'),Path('Untitled6.ipynb'),Path('Untitled11.ipynb'),Path('Untitled1-Copy1.ipynb'),Path('Untitled15.ipynb'),Path('Untitled2.ipynb'),Path('Untitled28.ipynb'),Path('Untitled17.ipynb'),Path('Untitled26.ipynb'),Path('Untitled24.ipynb'),Path('Untitled19.ipynb')...]
new_paths = nbs.map(rename_notebook)
Renamed Untitled12.ipynb to 2025-01-11-Fastai-Tokenizers.ipynb
Renamed Untitled5.ipynb to 2025-01-06-Fastlite-With-Files.ipynb
Renamed Untitled1.ipynb to 2025-01-04-Tone-Js-And-FastHTML.ipynb
Renamed Untitled16.ipynb to 2025-01-10-Untitled.ipynb
Renamed Untitled30.ipynb to 2025-01-29-Untitled.ipynb
Renamed Untitled29.ipynb to 2025-01-26-Numberblocks-6.ipynb
Renamed Untitled3.ipynb to 2025-01-05-Fast-HTML-By-Example-Part-2.ipynb
Renamed Untitled14.ipynb to 2025-01-14-London-Kolkata-Manila-Brisbane-Time-Conversion.ipynb
Renamed Untitled4.ipynb to 2025-01-06-Updating-Git-Repos-With-Unstaged-Changes-Check.ipynb
Renamed Untitled13.ipynb to 2025-01-12-Numberblock-2.ipynb
Renamed Untitled6.ipynb to 2025-01-06-SQLite-CLI-Power-Users-Guide.ipynb
Renamed Untitled11.ipynb to 2025-01-16-Using-FastCaddy-With-MonsterUI.ipynb
Renamed Untitled1-Copy1.ipynb to 2025-01-04-Check-Each-Repo-For-Uncommitted-Changes.ipynb
Renamed Untitled15.ipynb to 2025-01-11-Untitled-Notebook.ipynb
---------------------------------------------------------------------------
ResourceExhausted Traceback (most recent call last)
Cell In[94], line 1
----> 1 new_paths = nbs.map(rename_notebook)
File ~/git/fastcore/fastcore/foundation.py:163, in L.map(self, f, *args, **kwargs)
--> 163 def map(self, f, *args, **kwargs): return self._new(map_ex(self, f, *args, gen=False, **kwargs))
File ~/git/fastcore/fastcore/basics.py:927, in map_ex(iterable, f, gen, *args, **kwargs)
925 res = map(g, iterable)
926 if gen: return res
--> 927 return list(res)
File ~/git/fastcore/fastcore/basics.py:912, in bind.__call__(self, *args, **kwargs)
910 if isinstance(v,_Arg): kwargs[k] = args.pop(v.i)
911 fargs = [args[x.i] if isinstance(x, _Arg) else x for x in self.pargs] + args[self.maxi+1:]
--> 912 return self.func(*fargs, **kwargs)
Cell In[90], line 6, in rename_notebook(nb_path)
3 date = datetime.fromtimestamp(Path(nb_path).stat().st_mtime).strftime('%Y-%m-%d')
4 with open(nb_path) as f: nb = f.read()
----> 6 title_part = generate_title_part(nb)
8 new_name = f"{date}-{title_part.strip()}"
9 new_path = Path(new_name)
Cell In[72], line 20, in generate_title_part(nb)
2 prompt = f"""Given this Jupyter notebook, create a filename following these EXACT steps:
3 1. Extract the title from the first cell if it starts with '#'. In this case it's: "FastHTML By Example, Part 2"
4 2. Convert to the format: Words-In-Title-Case-With-Hyphens.ipynb
(...)
12
13 Return ONLY the filename, nothing else."""
14 safety_settings = [
15 {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE",},
16 {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE",},
17 {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE",},
18 {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE",},
19 ]
---> 20 response = model.generate_content(prompt, safety_settings=safety_settings, request_options = {"timeout": 1000})
21 try:
22 return response.text
File ~/.venv/lib/python3.12/site-packages/google/generativeai/generative_models.py:331, in GenerativeModel.generate_content(self, contents, generation_config, safety_settings, stream, tools, tool_config, request_options)
329 return generation_types.GenerateContentResponse.from_iterator(iterator)
330 else:
--> 331 response = self._client.generate_content(
332 request,
333 **request_options,
334 )
335 return generation_types.GenerateContentResponse.from_response(response)
336 except google.api_core.exceptions.InvalidArgument as e:
File ~/.venv/lib/python3.12/site-packages/google/ai/generativelanguage_v1beta/services/generative_service/client.py:830, in GenerativeServiceClient.generate_content(self, request, model, contents, retry, timeout, metadata)
827 self._validate_universe_domain()
829 # Send the request.
--> 830 response = rpc(
831 request,
832 retry=retry,
833 timeout=timeout,
834 metadata=metadata,
835 )
837 # Done; return the response.
838 return response
File ~/.venv/lib/python3.12/site-packages/google/api_core/gapic_v1/method.py:131, in _GapicCallable.__call__(self, timeout, retry, compression, *args, **kwargs)
128 if self._compression is not None:
129 kwargs["compression"] = compression
--> 131 return wrapped_func(*args, **kwargs)
File ~/.venv/lib/python3.12/site-packages/google/api_core/retry/retry_unary.py:293, in Retry.__call__.<locals>.retry_wrapped_func(*args, **kwargs)
289 target = functools.partial(func, *args, **kwargs)
290 sleep_generator = exponential_sleep_generator(
291 self._initial, self._maximum, multiplier=self._multiplier
292 )
--> 293 return retry_target(
294 target,
295 self._predicate,
296 sleep_generator,
297 timeout=self._timeout,
298 on_error=on_error,
299 )
File ~/.venv/lib/python3.12/site-packages/google/api_core/retry/retry_unary.py:153, in retry_target(target, predicate, sleep_generator, timeout, on_error, exception_factory, **kwargs)
149 # pylint: disable=broad-except
150 # This function explicitly must deal with broad exceptions.
151 except Exception as exc:
152 # defer to shared logic for handling errors
--> 153 _retry_error_helper(
154 exc,
155 deadline,
156 sleep,
157 error_list,
158 predicate,
159 on_error,
160 exception_factory,
161 timeout,
162 )
163 # if exception not raised, sleep before next attempt
164 time.sleep(sleep)
File ~/.venv/lib/python3.12/site-packages/google/api_core/retry/retry_base.py:212, in _retry_error_helper(exc, deadline, next_sleep, error_list, predicate_fn, on_error_fn, exc_factory_fn, original_timeout)
206 if not predicate_fn(exc):
207 final_exc, source_exc = exc_factory_fn(
208 error_list,
209 RetryFailureReason.NON_RETRYABLE_ERROR,
210 original_timeout,
211 )
--> 212 raise final_exc from source_exc
213 if on_error_fn is not None:
214 on_error_fn(exc)
File ~/.venv/lib/python3.12/site-packages/google/api_core/retry/retry_unary.py:144, in retry_target(target, predicate, sleep_generator, timeout, on_error, exception_factory, **kwargs)
142 for sleep in sleep_generator:
143 try:
--> 144 result = target()
145 if inspect.isawaitable(result):
146 warnings.warn(_ASYNC_RETRY_WARNING)
File ~/.venv/lib/python3.12/site-packages/google/api_core/timeout.py:120, in TimeToDeadlineTimeout.__call__.<locals>.func_with_timeout(*args, **kwargs)
117 # Avoid setting negative timeout
118 kwargs["timeout"] = max(0, self._timeout - time_since_first_attempt)
--> 120 return func(*args, **kwargs)
File ~/.venv/lib/python3.12/site-packages/google/api_core/grpc_helpers.py:78, in _wrap_unary_errors.<locals>.error_remapped_callable(*args, **kwargs)
76 return callable_(*args, **kwargs)
77 except grpc.RpcError as exc:
---> 78 raise exceptions.from_grpc_error(exc) from exc
ResourceExhausted: 429 Resource has been exhausted (e.g. check quota).
Several of my notebooks were renamed successfully! I ran out of quota, though. I was probably hitting the Gemini API too fast. Let's see where we are and try again.
nbs = L(Path().glob("Untitled*.ipynb"))
nbs
(#16) [Path('Untitled2.ipynb'),Path('Untitled28.ipynb'),Path('Untitled17.ipynb'),Path('Untitled26.ipynb'),Path('Untitled24.ipynb'),Path('Untitled19.ipynb'),Path('Untitled20.ipynb'),Path('Untitled8.ipynb'),Path('Untitled22.ipynb'),Path('Untitled18.ipynb'),Path('Untitled25.ipynb'),Path('Untitled9-Copy1.ipynb'),Path('Untitled27.ipynb'),Path('Untitled23.ipynb'),Path('Untitled9.ipynb'),Path('Untitled21.ipynb')]
new_paths = nbs.map(rename_notebook)
Renamed Untitled2.ipynb to 2025-01-03-Fast-HTML-By-Example-Part-2.ipynb
Renamed Untitled28.ipynb to 2025-01-23-Fast-HTML-By-Example-Part-2.ipynb
Warning: 2025-01-11-My-Daily-Notebook-Workflow.ipynb already exists
Renamed Untitled26.ipynb to 2025-01-23-Using-fastlite-and-apswutils.ipynb
Renamed Untitled24.ipynb to 2025-01-19-Exploring-Lucide-Icons.ipynb
Renamed Untitled19.ipynb to 2025-01-14-Discord-Message-Time-Converter.ipynb
Renamed Untitled20.ipynb to 2025-01-16-Fast-HTML-By-Example-Part-2.ipynb
Renamed Untitled8.ipynb to 2025-01-07-Use-Pathlib-For-Paths-Not-Env-Vars.ipynb
Renamed Untitled22.ipynb to 2025-01-17-AAI-Meeting-Notes-2025-01-17.ipynb
Renamed Untitled18.ipynb to 2025-01-12-Untitled.ipynb
Renamed Untitled25.ipynb to 2025-01-21-Making-CLI-Tools-With-Fastcore-Script.ipynb
Renamed Untitled9-Copy1.ipynb to 2025-01-07-FtResponse-In-FastHTML.ipynb
Renamed Untitled27.ipynb to 2025-01-23-Modifying-Execnb-Render_outputs-To-Use-Monsterui.ipynb
Renamed Untitled23.ipynb to 2025-01-26-Understanding-FastHTMLs-FT-Class.ipynb
Warning: 2025-01-07-FtResponse-In-FastHTML.ipynb already exists
Renamed Untitled21.ipynb to 2025-01-17-SVD-Finetuning-Exploration.ipynb
Mostly done! The 2 warnings sound like I have duplicates.
nbs = L(Path().glob("Untitled*.ipynb"))
nbs
(#2) [Path('Untitled17.ipynb'),Path('Untitled9.ipynb')]
Finally, I'm checking those remaining notebooks. It appears those are variations on the ones that exist. I can manually merge those.