Auto-Renaming My Untitled.ipynb Files With Gemini 1.5 Flash

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.