diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..b36f6fe Binary files /dev/null and b/.DS_Store differ diff --git a/David Whitmer Mini Project Data Capstone V8.ipynb b/David Whitmer Mini Project Data Capstone V8.ipynb new file mode 100644 index 0000000..d6a0a5b --- /dev/null +++ b/David Whitmer Mini Project Data Capstone V8.ipynb @@ -0,0 +1,913 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "a414e9e4-1a9c-48f2-897c-65dd421f6f27", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================================================================\n", + "TRISTRAM SHANDY: COMPLETE TEMPORAL ARCHITECTURE PIPELINE\n", + "Advanced Web Scraping + Dual-Panel Visualization + HTML Presentation\n", + "================================================================================\n", + "\n", + "Downloading Tristram Shandy from Project Gutenberg...\n", + "✓ Downloaded (1259736 characters)\n", + "Extracting chapters with full content...\n", + "✓ Extracted 300 chapters\n", + "Annotating chapters with narrative periods and selecting quotations...\n", + "✓ Annotated 300 chapters with quotations\n", + "\n", + "Creating dual-panel visualization...\n", + "✓ Visualization created\n", + "\n", + "Generating comprehensive HTML presentation...\n", + "✓ Complete HTML presentation saved: tristram_shandy_complete_analysis.html\n", + "\n", + "================================================================================\n", + "✓ COMPLETE PIPELINE FINISHED\n", + "================================================================================\n", + "\n", + "📄 Open this file in your browser: tristram_shandy_complete_analysis.html\n", + "\n", + "What you'll find:\n", + " • Comprehensive explanatory sections\n", + " • Dual-panel temporal architecture visualization\n", + " • Interactive quotations on hover\n", + " • Complete methodology and dataset summary\n", + "\n", + "The structure of non-linear time is now visible.\n", + "================================================================================\n", + "\n" + ] + } + ], + "source": [ + "\"\"\"\n", + "COMPLETE TRISTRAM SHANDY PIPELINE WITH HTML PRESENTATION\n", + "==========================================================\n", + "\n", + "This script:\n", + "1. Scrapes full text with intelligent quotation extraction\n", + "2. Creates dual-panel temporal architecture visualization\n", + "3. Generates comprehensive HTML page with explanatory sections\n", + "\n", + "Run this single script for the complete digital humanities presentation.\n", + "\"\"\"\n", + "\n", + "import requests\n", + "from bs4 import BeautifulSoup\n", + "import pandas as pd\n", + "import re\n", + "import json\n", + "from typing import List, Dict\n", + "import plotly.graph_objects as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "class TristramShandyCompletePipeline:\n", + " def __init__(self):\n", + " self.url = \"https://www.gutenberg.org/files/1079/1079-h/1079-h.htm\"\n", + " self.html_content = None\n", + " self.chapters = []\n", + " self.annotated_data = []\n", + " \n", + " # Narrative time buckets\n", + " self.time_buckets = {\n", + " \"dinah\": {\n", + " \"keywords\": [\"aunt dinah\", \"great aunt\", \"coachman\", \"family disgrace\"],\n", + " \"character_markers\": [\"dinah\"],\n", + " \"description\": \"Aunt Dinah's scandal\",\n", + " \"chronological_order\": -4\n", + " },\n", + " \"yorick_life\": {\n", + " \"keywords\": [\"yorick\", \"parson yorick\", \"denmark\", \"danish\", \"hamlet\"],\n", + " \"character_markers\": [\"yorick\", \"eugenius\"],\n", + " \"description\": \"Parson Yorick's life\",\n", + " \"chronological_order\": -3\n", + " },\n", + " \"toby_namur\": {\n", + " \"keywords\": [\"namur\", \"siege\", \"wound\", \"wounded\", \"groin\", \"flanders\", \"battle\"],\n", + " \"character_markers\": [\"toby\", \"uncle toby\"],\n", + " \"description\": \"Namur\",\n", + " \"chronological_order\": -2\n", + " },\n", + " \"toby_recovery\": {\n", + " \"keywords\": [\"fortification\", \"fortifications\", \"bowling green\", \"trim\", \n", + " \"corporal trim\", \"hobby-horse\", \"ramparts\", \"sieges\"],\n", + " \"character_markers\": [\"toby\", \"trim\", \"uncle toby\", \"corporal\"],\n", + " \"description\": \"Toby Recovery\",\n", + " \"chronological_order\": -1\n", + " },\n", + " \"conception\": {\n", + " \"keywords\": [\"conception\", \"begot\", \"begetting\", \"homunculus\", \"animal spirits\", \n", + " \"clock\", \"wind up\"],\n", + " \"character_markers\": [\"father\", \"mother\"],\n", + " \"description\": \"Conception\",\n", + " \"chronological_order\": 1\n", + " },\n", + " \"birth\": {\n", + " \"keywords\": [\"birth\", \"born\", \"midwife\", \"dr. slop\", \"slop\", \"delivery\", \n", + " \"forceps\", \"nose\", \"crushed\"],\n", + " \"character_markers\": [\"slop\", \"susannah\", \"mother\"],\n", + " \"description\": \"Birth\",\n", + " \"chronological_order\": 2\n", + " },\n", + " \"christening\": {\n", + " \"keywords\": [\"christening\", \"christened\", \"trismegistus\", \"baptism\", \"baptized\", \"name\"],\n", + " \"character_markers\": [\"curate\"],\n", + " \"description\": \"Christening\",\n", + " \"chronological_order\": 3\n", + " },\n", + " \"childhood_accidents\": {\n", + " \"keywords\": [\"circumcision\", \"window\", \"sash\", \"susannah\", \"accident\"],\n", + " \"character_markers\": [\"susannah\", \"obadiah\"],\n", + " \"description\": \"Childhood Accidents\",\n", + " \"chronological_order\": 4\n", + " },\n", + " \"childhood_education\": {\n", + " \"keywords\": [\"tristrapoedia\", \"governor\", \"education\", \"tutor\"],\n", + " \"character_markers\": [],\n", + " \"description\": \"Childhood Education\",\n", + " \"chronological_order\": 5\n", + " },\n", + " \"toby_wadman\": {\n", + " \"keywords\": [\"widow wadman\", \"wadman\", \"amours\", \"courtship\", \"love\", \"bridget\"],\n", + " \"character_markers\": [\"widow wadman\", \"bridget\"],\n", + " \"description\": \"Toby & Wadman\",\n", + " \"chronological_order\": 7\n", + " },\n", + " \"tristram_writing\": {\n", + " \"keywords\": [\"dear reader\", \"writing\", \"pen\", \"author\", \"narrative\", \"i am writing\"],\n", + " \"character_markers\": [\"dear sir\", \"reader\"],\n", + " \"description\": \"Tristram Writing\",\n", + " \"chronological_order\": 10\n", + " },\n", + " \"meta_commentary\": {\n", + " \"keywords\": [\"preface\", \"digression\", \"chapter about\", \"critic\", \"book\"],\n", + " \"character_markers\": [],\n", + " \"description\": \"Meta-commentary\",\n", + " \"chronological_order\": 11\n", + " },\n", + " \"walter_theories\": {\n", + " \"keywords\": [\"theory\", \"hypothesis\", \"auxiliary verbs\", \"nose theory\", \"names\"],\n", + " \"character_markers\": [\"father\", \"walter\"],\n", + " \"description\": \"Walter's Theories\",\n", + " \"chronological_order\": 0\n", + " },\n", + " \"slawkenbergius\": {\n", + " \"keywords\": [\"slawkenbergius\", \"strasbourg\", \"julia\", \"stranger\"],\n", + " \"character_markers\": [\"slawkenbergius\"],\n", + " \"description\": \"Slawkenbergius\",\n", + " \"chronological_order\": 0\n", + " },\n", + " \"sermon\": {\n", + " \"keywords\": [\"sermon\", \"conscience\", \"ernulphus\", \"preached\"],\n", + " \"character_markers\": [],\n", + " \"description\": \"Sermon\",\n", + " \"chronological_order\": 0\n", + " }\n", + " }\n", + " \n", + " # Color scheme\n", + " self.narrative_colors = {\n", + " 'tristram_writing': '#FF8C42',\n", + " 'toby_recovery': '#7FB069',\n", + " 'birth': '#4ECDC4',\n", + " 'toby_wadman': '#9B59B6',\n", + " 'toby_namur': '#E74C3C',\n", + " 'childhood_accidents': '#95A5A6',\n", + " 'conception': '#F1C40F',\n", + " 'meta_commentary': '#E67E22',\n", + " 'sermon': '#8E44AD',\n", + " 'walter_theories': '#C39BD3',\n", + " 'slawkenbergius': '#D7BDE2',\n", + " 'dinah': '#CB4335',\n", + " 'christening': '#45B7D1',\n", + " 'yorick_life': '#5DADE2',\n", + " 'childhood_education': '#F8B739'\n", + " }\n", + " \n", + " def download_book(self) -> bool:\n", + " print(\"Downloading Tristram Shandy from Project Gutenberg...\")\n", + " try:\n", + " response = requests.get(self.url, timeout=30)\n", + " response.raise_for_status()\n", + " self.html_content = response.text\n", + " print(f\"✓ Downloaded ({len(self.html_content)} characters)\")\n", + " return True\n", + " except Exception as e:\n", + " print(f\"✗ Error: {e}\")\n", + " return False\n", + " \n", + " def extract_chapters(self):\n", + " print(\"Extracting chapters with full content...\")\n", + " soup = BeautifulSoup(self.html_content, 'html.parser')\n", + " text_content = soup.get_text()\n", + " \n", + " chapter_pattern = re.compile(r'C\\s*H\\s*A\\s*P\\.?\\s+([IVXLCDM]+)', re.IGNORECASE)\n", + " matches = list(chapter_pattern.finditer(text_content))\n", + " \n", + " for i, match in enumerate(matches):\n", + " chapter_num_roman = match.group(1)\n", + " chapter_num = self._roman_to_int(chapter_num_roman)\n", + " start = match.end()\n", + " end = matches[i + 1].start() if i + 1 < len(matches) else len(text_content)\n", + " \n", + " content = text_content[start:end]\n", + " content = re.sub(r'\\s+', ' ', content).strip()\n", + " content = re.sub(r'\\[Pg \\d+\\]', '', content)\n", + " \n", + " if len(content) > 200:\n", + " self.chapters.append({\n", + " 'chapter': chapter_num,\n", + " 'chapter_roman': chapter_num_roman,\n", + " 'content': content\n", + " })\n", + " \n", + " print(f\"✓ Extracted {len(self.chapters)} chapters\")\n", + " \n", + " def _roman_to_int(self, roman: str) -> int:\n", + " roman = roman.upper().strip()\n", + " roman_values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}\n", + " total = 0\n", + " prev_value = 0\n", + " for char in reversed(roman):\n", + " value = roman_values.get(char, 0)\n", + " if value >= prev_value:\n", + " total += value\n", + " else:\n", + " total -= value\n", + " prev_value = value\n", + " return total\n", + " \n", + " def extract_candidate_quotations(self, content: str) -> List[Dict]:\n", + " \"\"\"Extract scored candidate quotations.\"\"\"\n", + " sentences = re.split(r'(?<=[.!?])\\s+', content)\n", + " candidates = []\n", + " \n", + " for i, sentence in enumerate(sentences):\n", + " if len(sentence) < 40 or len(sentence) > 400:\n", + " continue\n", + " if re.match(r'^(Chapter|CHAP|Vol)', sentence, re.IGNORECASE):\n", + " continue\n", + " \n", + " score = 0\n", + " sentence_lower = sentence.lower()\n", + " \n", + " # Character names\n", + " character_names = ['toby', 'trim', 'yorick', 'walter', 'slop', 'susannah', \n", + " 'obadiah', 'dinah', 'wadman', 'bobby']\n", + " for name in character_names:\n", + " if name in sentence_lower:\n", + " score += 2\n", + " \n", + " # Temporal markers\n", + " temporal_markers = ['when', 'after', 'before', 'during', 'since', 'until']\n", + " for marker in temporal_markers:\n", + " if marker in sentence_lower:\n", + " score += 1\n", + " \n", + " # Dialogue\n", + " if '\"' in sentence:\n", + " score += 1\n", + " \n", + " candidates.append({'text': sentence.strip(), 'score': score})\n", + " \n", + " return candidates\n", + " \n", + " def select_representative_quotation(self, content: str, narrative_bucket: str) -> str:\n", + " \"\"\"Select most representative quotation using keyword matching.\"\"\"\n", + " candidates = self.extract_candidate_quotations(content)\n", + " \n", + " if not candidates:\n", + " return content[:200] + \"...\"\n", + " \n", + " bucket_info = self.time_buckets.get(narrative_bucket, {})\n", + " keywords = bucket_info.get('keywords', [])\n", + " character_markers = bucket_info.get('character_markers', [])\n", + " \n", + " # Score by relevance to narrative bucket\n", + " for candidate in candidates:\n", + " quote_lower = candidate['text'].lower()\n", + " relevance = 0\n", + " \n", + " for keyword in keywords:\n", + " if keyword.lower() in quote_lower:\n", + " relevance += 3\n", + " \n", + " for character in character_markers:\n", + " if character.lower() in quote_lower:\n", + " relevance += 2\n", + " \n", + " candidate['relevance'] = relevance + candidate['score']\n", + " \n", + " candidates.sort(key=lambda x: x['relevance'], reverse=True)\n", + " return candidates[0]['text'] if candidates else content[:200] + \"...\"\n", + " \n", + " def annotate_chapters(self):\n", + " print(\"Annotating chapters with narrative periods and selecting quotations...\")\n", + " \n", + " for idx, chapter in enumerate(self.chapters):\n", + " content_lower = chapter['content'].lower()\n", + " \n", + " bucket_scores = {}\n", + " for bucket_name, bucket_info in self.time_buckets.items():\n", + " score = 0\n", + " for keyword in bucket_info['keywords']:\n", + " score += content_lower.count(keyword.lower()) * 2\n", + " for character in bucket_info.get('character_markers', []):\n", + " score += content_lower.count(character.lower()) * 3\n", + " if score > 0:\n", + " bucket_scores[bucket_name] = score\n", + " \n", + " primary_bucket = max(bucket_scores, key=bucket_scores.get) if bucket_scores else 'tristram_writing'\n", + " bucket_info = self.time_buckets[primary_bucket]\n", + " \n", + " representative_quote = self.select_representative_quotation(chapter['content'], primary_bucket)\n", + " \n", + " self.annotated_data.append({\n", + " 'chapter': chapter['chapter'],\n", + " 'chapter_roman': chapter['chapter_roman'],\n", + " 'reading_order': idx + 1,\n", + " 'chronological_position': bucket_info['chronological_order'],\n", + " 'narrative_period': primary_bucket,\n", + " 'narrative_description': bucket_info['description'],\n", + " 'representative_quote': representative_quote\n", + " })\n", + " \n", + " print(f\"✓ Annotated {len(self.annotated_data)} chapters with quotations\")\n", + " \n", + " def create_visualization(self) -> go.Figure:\n", + " \"\"\"Create the dual-panel temporal architecture visualization.\"\"\"\n", + " print(\"\\nCreating dual-panel visualization...\")\n", + " \n", + " df = pd.DataFrame(self.annotated_data)\n", + " df['color'] = df['narrative_period'].map(self.narrative_colors).fillna('#95A5A6')\n", + " \n", + " # Create dual-panel figure\n", + " fig = make_subplots(\n", + " rows=2, cols=1,\n", + " row_heights=[0.75, 0.25], # Give more space to top panel\n", + " vertical_spacing=0.10, # Slightly reduced spacing\n", + " subplot_titles=(\n", + " 'Narrative Path of \"Tristram Shandy\"',\n", + " 'Chronological Distribution'\n", + " ),\n", + " specs=[[{\"type\": \"scatter\"}], [{\"type\": \"bar\"}]]\n", + " )\n", + " \n", + " # TOP PANEL: Connected line graph\n", + " for i in range(len(df) - 1):\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " x=[df.iloc[i]['reading_order'], df.iloc[i+1]['reading_order']],\n", + " y=[df.iloc[i]['chronological_position'], df.iloc[i+1]['chronological_position']],\n", + " mode='lines',\n", + " line=dict(color=df.iloc[i]['color'], width=1.5), # Reduced from 2 to 1.5\n", + " showlegend=False,\n", + " hoverinfo='skip'\n", + " ),\n", + " row=1, col=1\n", + " )\n", + " \n", + " # Add markers with quotations\n", + " for period in df['narrative_period'].unique():\n", + " period_data = df[df['narrative_period'] == period]\n", + " \n", + " fig.add_trace(\n", + " go.Scatter(\n", + " x=period_data['reading_order'],\n", + " y=period_data['chronological_position'],\n", + " mode='markers',\n", + " marker=dict(\n", + " size=6, # Reduced from 7 to 6 for less visual clutter\n", + " color=self.narrative_colors.get(period, '#95A5A6'),\n", + " line=dict(width=0.5, color='white')\n", + " ),\n", + " name=period_data.iloc[0]['narrative_description'] if len(period_data) > 0 else period,\n", + " text=period_data.apply(\n", + " lambda row: f\"Chapter {row['chapter']}
\" +\n", + " f\"Period: {row['narrative_description']}

\" +\n", + " f\"\\\"{row['representative_quote'][:200]}{'...' if len(row['representative_quote']) > 200 else ''}\\\"\",\n", + " axis=1\n", + " ),\n", + " hovertemplate='%{text}',\n", + " legendgroup=period\n", + " ),\n", + " row=1, col=1\n", + " )\n", + " \n", + " # Add temporal labels\n", + " fig.add_annotation(x=10, y=-4, text=\"-4 Past\", showarrow=False,\n", + " font=dict(size=10, color='#7F8C8D'), xanchor='left', row=1, col=1)\n", + " fig.add_annotation(x=10, y=0, text=\"0 Digressions\", showarrow=False,\n", + " font=dict(size=10, color='#7F8C8D'), xanchor='left', row=1, col=1)\n", + " fig.add_annotation(x=10, y=11, text=\"11 Present\", showarrow=False,\n", + " font=dict(size=10, color='#7F8C8D'), xanchor='left', row=1, col=1)\n", + " \n", + " # BOTTOM PANEL: Distribution bars\n", + " chronological_counts = df.groupby(['chronological_position', 'narrative_period']).size().reset_index(name='count')\n", + " positions = sorted(df['chronological_position'].unique())\n", + " \n", + " for period in df['narrative_period'].unique():\n", + " period_counts = []\n", + " for pos in positions:\n", + " count = chronological_counts[\n", + " (chronological_counts['chronological_position'] == pos) & \n", + " (chronological_counts['narrative_period'] == period)\n", + " ]['count'].sum()\n", + " period_counts.append(count)\n", + " \n", + " fig.add_trace(\n", + " go.Bar(\n", + " x=positions,\n", + " y=period_counts,\n", + " name=df[df['narrative_period'] == period].iloc[0]['narrative_description'] if len(df[df['narrative_period'] == period]) > 0 else period,\n", + " marker_color=self.narrative_colors.get(period, '#95A5A6'),\n", + " showlegend=False,\n", + " legendgroup=period\n", + " ),\n", + " row=2, col=1\n", + " )\n", + " \n", + " # Layout\n", + " fig.update_xaxes(\n", + " title_text=\"Reading Order (Chapters)\", \n", + " showgrid=True,\n", + " gridwidth=0.3, # Thinner gridlines\n", + " gridcolor='rgba(200,200,200,0.25)', # Lighter gridlines\n", + " range=[0, df['reading_order'].max() + 10], # Add padding on right\n", + " row=1, col=1\n", + " )\n", + " fig.update_yaxes(\n", + " title_text=\"Chronological Position\", \n", + " showgrid=True,\n", + " gridwidth=0.3, # Thinner gridlines\n", + " gridcolor='rgba(200,200,200,0.25)', # Lighter gridlines\n", + " zeroline=True, \n", + " zerolinewidth=1, \n", + " zerolinecolor='rgba(100,100,100,0.4)',\n", + " tickmode='linear', \n", + " tick0=-4, \n", + " dtick=1,\n", + " range=[-5, 12], # Add padding top and bottom\n", + " row=1, col=1\n", + " )\n", + " fig.update_xaxes(\n", + " title_text=\"Chronological Position\", \n", + " tickmode='linear',\n", + " tick0=-4, \n", + " dtick=1, \n", + " row=2, col=1\n", + " )\n", + " fig.update_yaxes(\n", + " title_text=\"Number of Chapters\", \n", + " row=2, col=1\n", + " )\n", + " \n", + " fig.update_layout(\n", + " height=800,\n", + " width=1800, # Increased width for more horizontal space\n", + " plot_bgcolor='white',\n", + " paper_bgcolor='white',\n", + " font=dict(family=\"Arial, sans-serif\", size=11),\n", + " title_font=dict(size=16, color='#2C3E50'),\n", + " hovermode='closest',\n", + " margin=dict(l=80, r=250, t=80, b=60),\n", + " legend=dict(\n", + " title=dict(text=\"Narrative Periods\", font=dict(size=11)),\n", + " orientation=\"v\",\n", + " yanchor=\"top\",\n", + " y=0.95,\n", + " xanchor=\"left\",\n", + " x=1.01,\n", + " bgcolor=\"rgba(255,255,255,0.9)\",\n", + " bordercolor=\"#BDC3C7\",\n", + " borderwidth=1,\n", + " font=dict(size=9)\n", + " ),\n", + " barmode='stack'\n", + " )\n", + " \n", + " print(\"✓ Visualization created\")\n", + " return fig\n", + " \n", + " def create_html_presentation(self, fig: go.Figure):\n", + " \"\"\"Generate comprehensive HTML page with explanatory sections.\"\"\"\n", + " print(\"\\nGenerating comprehensive HTML presentation...\")\n", + " \n", + " df = pd.DataFrame(self.annotated_data)\n", + " \n", + " html_content = \"\"\"\n", + "\n", + "\n", + " \n", + " Tristram Shandy: Temporal Architecture Analysis\n", + " \n", + " \n", + "\n", + "\n", + "
\n", + "

THE LIFE AND OPINIONS OF TRISTRAM SHANDY, GENTLEMAN

\n", + "

by Laurence Sterne (1759-1767)

\n", + "

\n", + " A Digital Humanities Analysis of Non-Linear Narrative Structure\n", + "

\n", + "
\n", + "\n", + "
\n", + "

About the Novel

\n", + "

\n", + " The Life and Opinions of Tristram Shandy, Gentleman is one of literature's \n", + " first experimental novels and a masterpiece of non-linear storytelling. The narrator, \n", + " Tristram, attempts to tell his life story but constantly digresses into tangents, \n", + " backstories, and philosophical musings. The result is a chaotic, playful narrative \n", + " that jumps backward and forward in time.\n", + "

\n", + "
\n", + " 💡 Key Insight: Tristram famously doesn't even get to his own birth \n", + " until Volume 3 — he's too busy telling us about everything that led up to it! The \n", + " novel's structure itself becomes an argument about memory, digression, and the \n", + " impossibility of linear self-narration.\n", + "
\n", + "
\n", + "\n", + "
\n", + "

The Dual-Panel Visualization Explained

\n", + "

\n", + " This is not a generic timeline. It is a temporal architecture visualization \n", + " designed to make the novel's non-chronological structure legible. The visualization consists \n", + " of two synchronized panels:\n", + "

\n", + " \n", + "

Top Panel: Narrative Path Timeline

\n", + " \n", + "

\n", + " If the novel were linear, the line would ascend steadily from bottom-left \n", + " to top-right. Instead, you see dramatic vertical jumps—Sterne constantly yanking the reader \n", + " backward and forward through time.\n", + "

\n", + " \n", + "

Bottom Panel: Chronological Distribution

\n", + "

\n", + " A stacked bar chart showing how many chapters occupy each chronological position. This reveals \n", + " where the novel dwells chronologically—notice how much time Sterne spends on \"present \n", + " narration\" (position 10) compared to actual life events.\n", + "

\n", + "
\n", + "\n", + "
\n", + "

Understanding the Chronological Scale

\n", + " \n", + "
\n", + "
\n", + " -4 to -1\n", + "
Before Tristram's Birth
\n", + " Family history, Uncle Toby's war injury at Namur, Parson Yorick's life and death\n", + "
\n", + " \n", + "
\n", + " 0\n", + "
Digressions
\n", + " Timeless stories-within-stories, philosophical theories, embedded tales like Slawkenbergius\n", + "
\n", + " \n", + "
\n", + " 1 to 8\n", + "
Tristram's Life
\n", + " Conception → Birth → Christening → Childhood → Youth (chronological progression)\n", + "
\n", + " \n", + "
\n", + " 10-11\n", + "
Present Narrative
\n", + " Tristram writing the book, meta-commentary on the act of storytelling itself\n", + "
\n", + "
\n", + "\n", + "

Why Negative Numbers?

\n", + "

\n", + " Tristram doesn't start his story at his birth — he goes backward to tell us about \n", + " his family's history first. Uncle Toby's war wound at the Siege of Namur happened years \n", + " before Tristram was conceived, so it gets a negative chronological position (-2). \n", + " Think of position 0 as Tristram's birth (the origin point), with negative numbers representing \n", + " the years before that event. This shows how far back in time the narrative reaches before \n", + " the protagonist even exists.\n", + "

\n", + "
\n", + "\n", + "
\n", + "

What the Visualization Reveals

\n", + " \n", + "
\n", + " 💡 Literary Significance: The visualization quantifies narrative disorder. \n", + " A linear biography would show a smooth upward slope. Sterne's jagged line—with its wild \n", + " oscillations between past, present, and digression—reveals a narrator unable or unwilling \n", + " to tell his story in chronological order. The structure itself becomes an argument \n", + " about memory, digression, and the impossibility of linear self-narration.\n", + "
\n", + "
\n", + "\n", + "
\n", + "

Interactive Temporal Architecture

\n", + "

\n", + " How to interact: Hover over any point to see chapter details and representative \n", + " quotations. Click and drag to zoom into specific regions. Double-click to reset the view. \n", + " The line connects chapters in reading order—watch how it oscillates through time.\n", + "

\n", + "
\n", + "
\n", + "\n", + "
\n", + "

Methodology

\n", + " \n", + "
\n", + "

Data Collection & Processing

\n", + "

Source: Full text from Project Gutenberg

\n", + "

Chapters Analyzed: \"\"\" + str(len(df)) + \"\"\"

\n", + "

Quotation Selection: Automated intelligent extraction based on:

\n", + " \n", + "
\n", + " \n", + "

Dataset Summary

\n", + "

Total chapters analyzed: \"\"\" + str(len(df)) + \"\"\"

\n", + "

Reading order range: 1 to \"\"\" + str(df['reading_order'].max()) + \"\"\"

\n", + "

Chronological range: \"\"\" + str(df['chronological_position'].min()) + \"\"\" to \"\"\" + str(df['chronological_position'].max()) + \"\"\"

\n", + " \n", + "

Narrative Period Distribution

\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\"\"\"\n", + " \n", + " for bucket, count in df['narrative_period'].value_counts().items():\n", + " chrono_pos = df[df['narrative_period'] == bucket].iloc[0]['chronological_position']\n", + " description = df[df['narrative_period'] == bucket].iloc[0]['narrative_description']\n", + " html_content += f\"\"\" \n", + " \n", + " \n", + " \n", + " \n", + "\"\"\"\n", + " \n", + " html_content += \"\"\"
Narrative PeriodNumber of ChaptersChronological Position
{description}{count}{chrono_pos}
\n", + "
\n", + "\n", + "
\n", + "

\n", + " Citation: Sterne, Laurence. The Life and Opinions of Tristram Shandy, \n", + " Gentleman. Project Gutenberg. \n", + " \n", + " https://www.gutenberg.org/ebooks/1079\n", + " \n", + "

\n", + "
\n", + "\n", + " \n", + "\n", + "\n", + "\"\"\"\n", + " \n", + " filename = \"tristram_shandy_complete_analysis.html\"\n", + " with open(filename, 'w', encoding='utf-8') as f:\n", + " f.write(html_content)\n", + " \n", + " print(f\"✓ Complete HTML presentation saved: {filename}\")\n", + " return filename\n", + "\n", + "def main():\n", + " print(\"=\"*80)\n", + " print(\"TRISTRAM SHANDY: COMPLETE TEMPORAL ARCHITECTURE PIPELINE\")\n", + " print(\"Advanced Web Scraping + Dual-Panel Visualization + HTML Presentation\")\n", + " print(\"=\"*80 + \"\\n\")\n", + " \n", + " pipeline = TristramShandyCompletePipeline()\n", + " \n", + " # Step 1: Download\n", + " if not pipeline.download_book():\n", + " print(\"\\n✗ Failed to download. Exiting.\")\n", + " return\n", + " \n", + " # Step 2: Extract chapters\n", + " pipeline.extract_chapters()\n", + " \n", + " # Step 3: Annotate with intelligent quotation extraction\n", + " pipeline.annotate_chapters()\n", + " \n", + " # Step 4: Create visualization\n", + " fig = pipeline.create_visualization()\n", + " \n", + " # Step 5: Generate comprehensive HTML presentation\n", + " html_file = pipeline.create_html_presentation(fig)\n", + " \n", + " # Success\n", + " print(\"\\n\" + \"=\"*80)\n", + " print(\"✓ COMPLETE PIPELINE FINISHED\")\n", + " print(\"=\"*80)\n", + " print(f\"\\n📄 Open this file in your browser: {html_file}\")\n", + " print(\"\\nWhat you'll find:\")\n", + " print(\" • Comprehensive explanatory sections\")\n", + " print(\" • Dual-panel temporal architecture visualization\")\n", + " print(\" • Interactive quotations on hover\")\n", + " print(\" • Complete methodology and dataset summary\")\n", + " print(\"\\nThe structure of non-linear time is now visible.\")\n", + " print(\"=\"*80 + \"\\n\")\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94c7c0c1-f5de-4ba7-9b83-eae8a41d5c4a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/David Whitmer Mini Project Proposal Idea. .docx b/David Whitmer Mini Project Proposal Idea. .docx new file mode 100644 index 0000000..5caa646 Binary files /dev/null and b/David Whitmer Mini Project Proposal Idea. .docx differ diff --git a/Mini Project Data Capstone V8.md b/Mini Project Data Capstone V8.md new file mode 100644 index 0000000..f21f002 --- /dev/null +++ b/Mini Project Data Capstone V8.md @@ -0,0 +1,873 @@ +```python +""" +COMPLETE TRISTRAM SHANDY PIPELINE WITH HTML PRESENTATION +========================================================== + +This script: +1. Scrapes full text with intelligent quotation extraction +2. Creates dual-panel temporal architecture visualization +3. Generates comprehensive HTML page with explanatory sections + +Run this single script for the complete digital humanities presentation. +""" + +import requests +from bs4 import BeautifulSoup +import pandas as pd +import re +import json +from typing import List, Dict +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +class TristramShandyCompletePipeline: + def __init__(self): + self.url = "https://www.gutenberg.org/files/1079/1079-h/1079-h.htm" + self.html_content = None + self.chapters = [] + self.annotated_data = [] + + # Narrative time buckets + self.time_buckets = { + "dinah": { + "keywords": ["aunt dinah", "great aunt", "coachman", "family disgrace"], + "character_markers": ["dinah"], + "description": "Aunt Dinah's scandal", + "chronological_order": -4 + }, + "yorick_life": { + "keywords": ["yorick", "parson yorick", "denmark", "danish", "hamlet"], + "character_markers": ["yorick", "eugenius"], + "description": "Parson Yorick's life", + "chronological_order": -3 + }, + "toby_namur": { + "keywords": ["namur", "siege", "wound", "wounded", "groin", "flanders", "battle"], + "character_markers": ["toby", "uncle toby"], + "description": "Namur", + "chronological_order": -2 + }, + "toby_recovery": { + "keywords": ["fortification", "fortifications", "bowling green", "trim", + "corporal trim", "hobby-horse", "ramparts", "sieges"], + "character_markers": ["toby", "trim", "uncle toby", "corporal"], + "description": "Toby Recovery", + "chronological_order": -1 + }, + "conception": { + "keywords": ["conception", "begot", "begetting", "homunculus", "animal spirits", + "clock", "wind up"], + "character_markers": ["father", "mother"], + "description": "Conception", + "chronological_order": 1 + }, + "birth": { + "keywords": ["birth", "born", "midwife", "dr. slop", "slop", "delivery", + "forceps", "nose", "crushed"], + "character_markers": ["slop", "susannah", "mother"], + "description": "Birth", + "chronological_order": 2 + }, + "christening": { + "keywords": ["christening", "christened", "trismegistus", "baptism", "baptized", "name"], + "character_markers": ["curate"], + "description": "Christening", + "chronological_order": 3 + }, + "childhood_accidents": { + "keywords": ["circumcision", "window", "sash", "susannah", "accident"], + "character_markers": ["susannah", "obadiah"], + "description": "Childhood Accidents", + "chronological_order": 4 + }, + "childhood_education": { + "keywords": ["tristrapoedia", "governor", "education", "tutor"], + "character_markers": [], + "description": "Childhood Education", + "chronological_order": 5 + }, + "toby_wadman": { + "keywords": ["widow wadman", "wadman", "amours", "courtship", "love", "bridget"], + "character_markers": ["widow wadman", "bridget"], + "description": "Toby & Wadman", + "chronological_order": 7 + }, + "tristram_writing": { + "keywords": ["dear reader", "writing", "pen", "author", "narrative", "i am writing"], + "character_markers": ["dear sir", "reader"], + "description": "Tristram Writing", + "chronological_order": 10 + }, + "meta_commentary": { + "keywords": ["preface", "digression", "chapter about", "critic", "book"], + "character_markers": [], + "description": "Meta-commentary", + "chronological_order": 11 + }, + "walter_theories": { + "keywords": ["theory", "hypothesis", "auxiliary verbs", "nose theory", "names"], + "character_markers": ["father", "walter"], + "description": "Walter's Theories", + "chronological_order": 0 + }, + "slawkenbergius": { + "keywords": ["slawkenbergius", "strasbourg", "julia", "stranger"], + "character_markers": ["slawkenbergius"], + "description": "Slawkenbergius", + "chronological_order": 0 + }, + "sermon": { + "keywords": ["sermon", "conscience", "ernulphus", "preached"], + "character_markers": [], + "description": "Sermon", + "chronological_order": 0 + } + } + + # Color scheme + self.narrative_colors = { + 'tristram_writing': '#FF8C42', + 'toby_recovery': '#7FB069', + 'birth': '#4ECDC4', + 'toby_wadman': '#9B59B6', + 'toby_namur': '#E74C3C', + 'childhood_accidents': '#95A5A6', + 'conception': '#F1C40F', + 'meta_commentary': '#E67E22', + 'sermon': '#8E44AD', + 'walter_theories': '#C39BD3', + 'slawkenbergius': '#D7BDE2', + 'dinah': '#CB4335', + 'christening': '#45B7D1', + 'yorick_life': '#5DADE2', + 'childhood_education': '#F8B739' + } + + def download_book(self) -> bool: + print("Downloading Tristram Shandy from Project Gutenberg...") + try: + response = requests.get(self.url, timeout=30) + response.raise_for_status() + self.html_content = response.text + print(f"✓ Downloaded ({len(self.html_content)} characters)") + return True + except Exception as e: + print(f"✗ Error: {e}") + return False + + def extract_chapters(self): + print("Extracting chapters with full content...") + soup = BeautifulSoup(self.html_content, 'html.parser') + text_content = soup.get_text() + + chapter_pattern = re.compile(r'C\s*H\s*A\s*P\.?\s+([IVXLCDM]+)', re.IGNORECASE) + matches = list(chapter_pattern.finditer(text_content)) + + for i, match in enumerate(matches): + chapter_num_roman = match.group(1) + chapter_num = self._roman_to_int(chapter_num_roman) + start = match.end() + end = matches[i + 1].start() if i + 1 < len(matches) else len(text_content) + + content = text_content[start:end] + content = re.sub(r'\s+', ' ', content).strip() + content = re.sub(r'\[Pg \d+\]', '', content) + + if len(content) > 200: + self.chapters.append({ + 'chapter': chapter_num, + 'chapter_roman': chapter_num_roman, + 'content': content + }) + + print(f"✓ Extracted {len(self.chapters)} chapters") + + def _roman_to_int(self, roman: str) -> int: + roman = roman.upper().strip() + roman_values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} + total = 0 + prev_value = 0 + for char in reversed(roman): + value = roman_values.get(char, 0) + if value >= prev_value: + total += value + else: + total -= value + prev_value = value + return total + + def extract_candidate_quotations(self, content: str) -> List[Dict]: + """Extract scored candidate quotations.""" + sentences = re.split(r'(?<=[.!?])\s+', content) + candidates = [] + + for i, sentence in enumerate(sentences): + if len(sentence) < 40 or len(sentence) > 400: + continue + if re.match(r'^(Chapter|CHAP|Vol)', sentence, re.IGNORECASE): + continue + + score = 0 + sentence_lower = sentence.lower() + + # Character names + character_names = ['toby', 'trim', 'yorick', 'walter', 'slop', 'susannah', + 'obadiah', 'dinah', 'wadman', 'bobby'] + for name in character_names: + if name in sentence_lower: + score += 2 + + # Temporal markers + temporal_markers = ['when', 'after', 'before', 'during', 'since', 'until'] + for marker in temporal_markers: + if marker in sentence_lower: + score += 1 + + # Dialogue + if '"' in sentence: + score += 1 + + candidates.append({'text': sentence.strip(), 'score': score}) + + return candidates + + def select_representative_quotation(self, content: str, narrative_bucket: str) -> str: + """Select most representative quotation using keyword matching.""" + candidates = self.extract_candidate_quotations(content) + + if not candidates: + return content[:200] + "..." + + bucket_info = self.time_buckets.get(narrative_bucket, {}) + keywords = bucket_info.get('keywords', []) + character_markers = bucket_info.get('character_markers', []) + + # Score by relevance to narrative bucket + for candidate in candidates: + quote_lower = candidate['text'].lower() + relevance = 0 + + for keyword in keywords: + if keyword.lower() in quote_lower: + relevance += 3 + + for character in character_markers: + if character.lower() in quote_lower: + relevance += 2 + + candidate['relevance'] = relevance + candidate['score'] + + candidates.sort(key=lambda x: x['relevance'], reverse=True) + return candidates[0]['text'] if candidates else content[:200] + "..." + + def annotate_chapters(self): + print("Annotating chapters with narrative periods and selecting quotations...") + + for idx, chapter in enumerate(self.chapters): + content_lower = chapter['content'].lower() + + bucket_scores = {} + for bucket_name, bucket_info in self.time_buckets.items(): + score = 0 + for keyword in bucket_info['keywords']: + score += content_lower.count(keyword.lower()) * 2 + for character in bucket_info.get('character_markers', []): + score += content_lower.count(character.lower()) * 3 + if score > 0: + bucket_scores[bucket_name] = score + + primary_bucket = max(bucket_scores, key=bucket_scores.get) if bucket_scores else 'tristram_writing' + bucket_info = self.time_buckets[primary_bucket] + + representative_quote = self.select_representative_quotation(chapter['content'], primary_bucket) + + self.annotated_data.append({ + 'chapter': chapter['chapter'], + 'chapter_roman': chapter['chapter_roman'], + 'reading_order': idx + 1, + 'chronological_position': bucket_info['chronological_order'], + 'narrative_period': primary_bucket, + 'narrative_description': bucket_info['description'], + 'representative_quote': representative_quote + }) + + print(f"✓ Annotated {len(self.annotated_data)} chapters with quotations") + + def create_visualization(self) -> go.Figure: + """Create the dual-panel temporal architecture visualization.""" + print("\nCreating dual-panel visualization...") + + df = pd.DataFrame(self.annotated_data) + df['color'] = df['narrative_period'].map(self.narrative_colors).fillna('#95A5A6') + + # Create dual-panel figure + fig = make_subplots( + rows=2, cols=1, + row_heights=[0.75, 0.25], # Give more space to top panel + vertical_spacing=0.10, # Slightly reduced spacing + subplot_titles=( + 'Narrative Path of "Tristram Shandy"', + 'Chronological Distribution' + ), + specs=[[{"type": "scatter"}], [{"type": "bar"}]] + ) + + # TOP PANEL: Connected line graph + for i in range(len(df) - 1): + fig.add_trace( + go.Scatter( + x=[df.iloc[i]['reading_order'], df.iloc[i+1]['reading_order']], + y=[df.iloc[i]['chronological_position'], df.iloc[i+1]['chronological_position']], + mode='lines', + line=dict(color=df.iloc[i]['color'], width=1.5), # Reduced from 2 to 1.5 + showlegend=False, + hoverinfo='skip' + ), + row=1, col=1 + ) + + # Add markers with quotations + for period in df['narrative_period'].unique(): + period_data = df[df['narrative_period'] == period] + + fig.add_trace( + go.Scatter( + x=period_data['reading_order'], + y=period_data['chronological_position'], + mode='markers', + marker=dict( + size=6, # Reduced from 7 to 6 for less visual clutter + color=self.narrative_colors.get(period, '#95A5A6'), + line=dict(width=0.5, color='white') + ), + name=period_data.iloc[0]['narrative_description'] if len(period_data) > 0 else period, + text=period_data.apply( + lambda row: f"Chapter {row['chapter']}
" + + f"Period: {row['narrative_description']}

" + + f"\"{row['representative_quote'][:200]}{'...' if len(row['representative_quote']) > 200 else ''}\"", + axis=1 + ), + hovertemplate='%{text}', + legendgroup=period + ), + row=1, col=1 + ) + + # Add temporal labels + fig.add_annotation(x=10, y=-4, text="-4 Past", showarrow=False, + font=dict(size=10, color='#7F8C8D'), xanchor='left', row=1, col=1) + fig.add_annotation(x=10, y=0, text="0 Digressions", showarrow=False, + font=dict(size=10, color='#7F8C8D'), xanchor='left', row=1, col=1) + fig.add_annotation(x=10, y=11, text="11 Present", showarrow=False, + font=dict(size=10, color='#7F8C8D'), xanchor='left', row=1, col=1) + + # BOTTOM PANEL: Distribution bars + chronological_counts = df.groupby(['chronological_position', 'narrative_period']).size().reset_index(name='count') + positions = sorted(df['chronological_position'].unique()) + + for period in df['narrative_period'].unique(): + period_counts = [] + for pos in positions: + count = chronological_counts[ + (chronological_counts['chronological_position'] == pos) & + (chronological_counts['narrative_period'] == period) + ]['count'].sum() + period_counts.append(count) + + fig.add_trace( + go.Bar( + x=positions, + y=period_counts, + name=df[df['narrative_period'] == period].iloc[0]['narrative_description'] if len(df[df['narrative_period'] == period]) > 0 else period, + marker_color=self.narrative_colors.get(period, '#95A5A6'), + showlegend=False, + legendgroup=period + ), + row=2, col=1 + ) + + # Layout + fig.update_xaxes( + title_text="Reading Order (Chapters)", + showgrid=True, + gridwidth=0.3, # Thinner gridlines + gridcolor='rgba(200,200,200,0.25)', # Lighter gridlines + range=[0, df['reading_order'].max() + 10], # Add padding on right + row=1, col=1 + ) + fig.update_yaxes( + title_text="Chronological Position", + showgrid=True, + gridwidth=0.3, # Thinner gridlines + gridcolor='rgba(200,200,200,0.25)', # Lighter gridlines + zeroline=True, + zerolinewidth=1, + zerolinecolor='rgba(100,100,100,0.4)', + tickmode='linear', + tick0=-4, + dtick=1, + range=[-5, 12], # Add padding top and bottom + row=1, col=1 + ) + fig.update_xaxes( + title_text="Chronological Position", + tickmode='linear', + tick0=-4, + dtick=1, + row=2, col=1 + ) + fig.update_yaxes( + title_text="Number of Chapters", + row=2, col=1 + ) + + fig.update_layout( + height=800, + width=1800, # Increased width for more horizontal space + plot_bgcolor='white', + paper_bgcolor='white', + font=dict(family="Arial, sans-serif", size=11), + title_font=dict(size=16, color='#2C3E50'), + hovermode='closest', + margin=dict(l=80, r=250, t=80, b=60), + legend=dict( + title=dict(text="Narrative Periods", font=dict(size=11)), + orientation="v", + yanchor="top", + y=0.95, + xanchor="left", + x=1.01, + bgcolor="rgba(255,255,255,0.9)", + bordercolor="#BDC3C7", + borderwidth=1, + font=dict(size=9) + ), + barmode='stack' + ) + + print("✓ Visualization created") + return fig + + def create_html_presentation(self, fig: go.Figure): + """Generate comprehensive HTML page with explanatory sections.""" + print("\nGenerating comprehensive HTML presentation...") + + df = pd.DataFrame(self.annotated_data) + + html_content = """ + + + + Tristram Shandy: Temporal Architecture Analysis + + + + +
+

THE LIFE AND OPINIONS OF TRISTRAM SHANDY, GENTLEMAN

+

by Laurence Sterne (1759-1767)

+

+ A Digital Humanities Analysis of Non-Linear Narrative Structure +

+
+ +
+

About the Novel

+

+ The Life and Opinions of Tristram Shandy, Gentleman is one of literature's + first experimental novels and a masterpiece of non-linear storytelling. The narrator, + Tristram, attempts to tell his life story but constantly digresses into tangents, + backstories, and philosophical musings. The result is a chaotic, playful narrative + that jumps backward and forward in time. +

+
+ 💡 Key Insight: Tristram famously doesn't even get to his own birth + until Volume 3 — he's too busy telling us about everything that led up to it! The + novel's structure itself becomes an argument about memory, digression, and the + impossibility of linear self-narration. +
+
+ +
+

The Dual-Panel Visualization Explained

+

+ This is not a generic timeline. It is a temporal architecture visualization + designed to make the novel's non-chronological structure legible. The visualization consists + of two synchronized panels: +

+ +

Top Panel: Narrative Path Timeline

+ +

+ If the novel were linear, the line would ascend steadily from bottom-left + to top-right. Instead, you see dramatic vertical jumps—Sterne constantly yanking the reader + backward and forward through time. +

+ +

Bottom Panel: Chronological Distribution

+

+ A stacked bar chart showing how many chapters occupy each chronological position. This reveals + where the novel dwells chronologically—notice how much time Sterne spends on "present + narration" (position 10) compared to actual life events. +

+
+ +
+

Understanding the Chronological Scale

+ +
+
+ -4 to -1 +
Before Tristram's Birth
+ Family history, Uncle Toby's war injury at Namur, Parson Yorick's life and death +
+ +
+ 0 +
Digressions
+ Timeless stories-within-stories, philosophical theories, embedded tales like Slawkenbergius +
+ +
+ 1 to 8 +
Tristram's Life
+ Conception → Birth → Christening → Childhood → Youth (chronological progression) +
+ +
+ 10-11 +
Present Narrative
+ Tristram writing the book, meta-commentary on the act of storytelling itself +
+
+ +

Why Negative Numbers?

+

+ Tristram doesn't start his story at his birth — he goes backward to tell us about + his family's history first. Uncle Toby's war wound at the Siege of Namur happened years + before Tristram was conceived, so it gets a negative chronological position (-2). + Think of position 0 as Tristram's birth (the origin point), with negative numbers representing + the years before that event. This shows how far back in time the narrative reaches before + the protagonist even exists. +

+
+ +
+

What the Visualization Reveals

+ +
+ 💡 Literary Significance: The visualization quantifies narrative disorder. + A linear biography would show a smooth upward slope. Sterne's jagged line—with its wild + oscillations between past, present, and digression—reveals a narrator unable or unwilling + to tell his story in chronological order. The structure itself becomes an argument + about memory, digression, and the impossibility of linear self-narration. +
+
+ +
+

Interactive Temporal Architecture

+

+ How to interact: Hover over any point to see chapter details and representative + quotations. Click and drag to zoom into specific regions. Double-click to reset the view. + The line connects chapters in reading order—watch how it oscillates through time. +

+
+
+ +
+

Methodology

+ +
+

Data Collection & Processing

+

Source: Full text from Project Gutenberg

+

Chapters Analyzed: """ + str(len(df)) + """

+

Quotation Selection: Automated intelligent extraction based on:

+ +
+ +

Dataset Summary

+

Total chapters analyzed: """ + str(len(df)) + """

+

Reading order range: 1 to """ + str(df['reading_order'].max()) + """

+

Chronological range: """ + str(df['chronological_position'].min()) + """ to """ + str(df['chronological_position'].max()) + """

+ +

Narrative Period Distribution

+ + + + + + +""" + + for bucket, count in df['narrative_period'].value_counts().items(): + chrono_pos = df[df['narrative_period'] == bucket].iloc[0]['chronological_position'] + description = df[df['narrative_period'] == bucket].iloc[0]['narrative_description'] + html_content += f""" + + + + +""" + + html_content += """
Narrative PeriodNumber of ChaptersChronological Position
{description}{count}{chrono_pos}
+
+ +
+

+ Citation: Sterne, Laurence. The Life and Opinions of Tristram Shandy, + Gentleman. Project Gutenberg. + + https://www.gutenberg.org/ebooks/1079 + +

+
+ + + + +""" + + filename = "tristram_shandy_complete_analysis.html" + with open(filename, 'w', encoding='utf-8') as f: + f.write(html_content) + + print(f"✓ Complete HTML presentation saved: {filename}") + return filename + +def main(): + print("="*80) + print("TRISTRAM SHANDY: COMPLETE TEMPORAL ARCHITECTURE PIPELINE") + print("Advanced Web Scraping + Dual-Panel Visualization + HTML Presentation") + print("="*80 + "\n") + + pipeline = TristramShandyCompletePipeline() + + # Step 1: Download + if not pipeline.download_book(): + print("\n✗ Failed to download. Exiting.") + return + + # Step 2: Extract chapters + pipeline.extract_chapters() + + # Step 3: Annotate with intelligent quotation extraction + pipeline.annotate_chapters() + + # Step 4: Create visualization + fig = pipeline.create_visualization() + + # Step 5: Generate comprehensive HTML presentation + html_file = pipeline.create_html_presentation(fig) + + # Success + print("\n" + "="*80) + print("✓ COMPLETE PIPELINE FINISHED") + print("="*80) + print(f"\n📄 Open this file in your browser: {html_file}") + print("\nWhat you'll find:") + print(" • Comprehensive explanatory sections") + print(" • Dual-panel temporal architecture visualization") + print(" • Interactive quotations on hover") + print(" • Complete methodology and dataset summary") + print("\nThe structure of non-linear time is now visible.") + print("="*80 + "\n") + +if __name__ == "__main__": + main() +``` + + ================================================================================ + TRISTRAM SHANDY: COMPLETE TEMPORAL ARCHITECTURE PIPELINE + Advanced Web Scraping + Dual-Panel Visualization + HTML Presentation + ================================================================================ + + Downloading Tristram Shandy from Project Gutenberg... + ✓ Downloaded (1259736 characters) + Extracting chapters with full content... + ✓ Extracted 300 chapters + Annotating chapters with narrative periods and selecting quotations... + ✓ Annotated 300 chapters with quotations + + Creating dual-panel visualization... + ✓ Visualization created + + Generating comprehensive HTML presentation... + ✓ Complete HTML presentation saved: tristram_shandy_complete_analysis.html + + ================================================================================ + ✓ COMPLETE PIPELINE FINISHED + ================================================================================ + + 📄 Open this file in your browser: tristram_shandy_complete_analysis.html + + What you'll find: + • Comprehensive explanatory sections + • Dual-panel temporal architecture visualization + • Interactive quotations on hover + • Complete methodology and dataset summary + + The structure of non-linear time is now visible. + ================================================================================ + + + + +```python + +``` diff --git a/Mini Project Proposal Idea 2.docx b/Mini Project Proposal Idea 2.docx new file mode 100644 index 0000000..cf60372 Binary files /dev/null and b/Mini Project Proposal Idea 2.docx differ diff --git a/Project_Idea.md b/Project_Idea.md new file mode 100644 index 0000000..b22db26 --- /dev/null +++ b/Project_Idea.md @@ -0,0 +1,8 @@ +# Project Idea +# David Whitmer +For my three-course sequence to complete the Data Analytics major, I chose English. The goal of this project is to apply data-driven methods to examine narrative time and setting in Karl Ove Knausgaard’s A Death in the Family. Specifically, this project seeks to map the non-chronological movement of the narrative by tracking shifts in temporal position and setting throughout the text. The proposed research question is: How does Knausgaard’s use of non-linear chronology and recurring settings structure the reader’s experience of memory and narration in A Death in the Family? Ultimately, the goal of the project is to help literary scholars and readers make sense of the novel’s fragmented temporal structure by visualizing how the narrative moves across time and place. +The primary text for this project is A Death in the Family, the first volume of Knausgaard’s My Life series. I own a digital copy of the novel, and because the book spans approximately 600 pages, it provides a substantial and ethically sound dataset for analysis. The length and density of the text make it particularly well suited for a data analytics approach, as the narrative repeatedly shifts between different moments in Knausgaard’s life and revisits key locations across decades. +The data for this project will be created through close reading and structured annotation of the text. Each unit of analysis (such as a paragraph or page range) will be tagged for narrative time (for example, childhood, adolescence, early adulthood, or present-day narration following the father’s death) and setting (such as the childhood home, the father’s house, or other significant locations). These annotations will then be organized into a dataset that reflects both the reading order of the novel and the underlying chronological order of the events being narrated. +Once the dataset is constructed, the primary analytical method will be visualization rather than inferential modeling. The central output of the project will be an interactive timeline that displays narrative time and setting in chronological order. When a user hovers over a specific date or time period on the timeline, a representative quotation from the novel will appear, allowing readers to see how Knausgaard’s prose inhabits that moment. By comparing reading order to narrative chronology, the visualization will make visible the extent to which the novel resists linear storytelling and instead organizes memory spatially and temporally. +The intended audience for this project includes literary scholars, students, and readers of contemporary autofiction who may struggle to track the novel’s complex temporal structure through traditional close reading alone. By providing a visual and interactive tool, the project offers a new way to engage with the text that complements, rather than replaces, interpretive literary analysis. The results may also contribute to broader discussions in literary studies about memory, autobiography, and non-linear narrative form. +Ethically, the project poses minimal concerns. Because I own a digital copy of the text, the data is accessed legally and for educational purposes. The project does not involve personal data from living individuals beyond the author himself, and all quotations used in the visualization will be properly cited and limited in length. The goal is not to reproduce the text but to analyze and contextualize it. Overall, this project demonstrates how data analytics methods can be used responsibly to illuminate complex narrative structures in contemporary literature. diff --git a/presentations/.DS_Store b/presentations/.DS_Store new file mode 100644 index 0000000..2851ba4 Binary files /dev/null and b/presentations/.DS_Store differ diff --git a/presentations/David Whitmer Test.Rmd b/presentations/David Whitmer Test.Rmd new file mode 100644 index 0000000..97802d7 --- /dev/null +++ b/presentations/David Whitmer Test.Rmd @@ -0,0 +1,20 @@ +--- +title: "My presentation" +subtitle: "⚔
with xaringan" +author: "David Whitmer" +institute: "RStudio, PBC" +date: "2016/12/12 (updated: `r Sys.Date()`)" +output: + xaringan::moon_reader: + css: xaringan-themer.css + lib_dir: libs + nature: + highlightStyle: github + highlightLines: true + countIncrementalSlides: false +--- + + + + +# This is a test presentation! diff --git a/presentations/test123.html b/presentations/David Whitmer.html similarity index 98% rename from presentations/test123.html rename to presentations/David Whitmer.html index 41fee02..a431152 100644 --- a/presentations/test123.html +++ b/presentations/David Whitmer.html @@ -3,7 +3,7 @@ My presentation - + @@ -18,7 +18,7 @@ ## ⚔
with xaringan ] .author[ -### Eren Bilen +### David Whitmer ] .institute[ ### RStudio, PBC diff --git a/presentations/libs/header-attrs-2.30/header-attrs.js b/presentations/libs/header-attrs-2.30/header-attrs.js new file mode 100644 index 0000000..dd57d92 --- /dev/null +++ b/presentations/libs/header-attrs-2.30/header-attrs.js @@ -0,0 +1,12 @@ +// Pandoc 2.9 adds attributes on both header and div. We remove the former (to +// be compatible with the behavior of Pandoc < 2.8). +document.addEventListener('DOMContentLoaded', function(e) { + var hs = document.querySelectorAll("div.section[class*='level'] > :first-child"); + var i, h, a; + for (i = 0; i < hs.length; i++) { + h = hs[i]; + if (!/^h[1-6]$/i.test(h.tagName)) continue; // it should be a header h1-h6 + a = h.attributes; + while (a.length > 0) h.removeAttribute(a[0].name); + } +}); diff --git a/presentations/test123.Rmd b/presentations/test123.Rmd index 1cdb3e7..97802d7 100644 --- a/presentations/test123.Rmd +++ b/presentations/test123.Rmd @@ -1,7 +1,7 @@ --- title: "My presentation" subtitle: "⚔
with xaringan" -author: "Eren Bilen" +author: "David Whitmer" institute: "RStudio, PBC" date: "2016/12/12 (updated: `r Sys.Date()`)" output: @@ -14,15 +14,7 @@ output: countIncrementalSlides: false --- -```{r xaringan-themer, include=FALSE, warning=FALSE} -library(xaringanthemer) -style_mono_accent( - base_color = "#1c5253", - header_font_google = google_font("Josefin Sans"), - text_font_google = google_font("Montserrat", "300", "300i"), - code_font_google = google_font("Fira Mono") -) -``` + # This is a test presentation! diff --git a/tristram_final_fixed.Rmd b/tristram_final_fixed.Rmd new file mode 100644 index 0000000..57e28e5 --- /dev/null +++ b/tristram_final_fixed.Rmd @@ -0,0 +1,512 @@ +--- +title: "Mapping Narrative Time in Tristram Shandy" +subtitle: "A Computational Model of Temporal Digression" +author: "David Whitmer" +institute: "Dickinson College | Data Capstone 400 Mini Project" +date: "February 17, 2026" +output: + xaringan::moon_reader: + lib_dir: libs + nature: + highlightStyle: github + highlightLines: true + countIncrementalSlides: false + ratio: "16:9" + css: [default, default-fonts] +--- + +```{r setup, include=FALSE} +options(htmltools.dir.version = FALSE) +knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE) +library(knitr) +``` + +```{css, echo=FALSE} +.title-slide { + background-color: #ffffff; + color: #000000; +} + +.title-slide h1 { + font-size: 2.5em; + font-weight: 600; + margin-bottom: 0.3em; + color: #000000; +} + +.title-slide h2 { + font-size: 1.5em; + font-weight: 300; + margin-bottom: 1em; + color: #000000; +} + +.title-slide h3 { + font-size: 1.2em; + font-weight: 400; + margin-top: 2em; + color: #000000; +} + +.remark-slide-content { + font-size: 24px; + padding: 1em 2em 1em 2em; +} + +.remark-slide-content h1 { + font-size: 2em; + color: #2c3e50; + margin-bottom: 0.5em; +} + +.remark-slide-content h2 { + font-size: 1.5em; + color: #34495e; + margin-bottom: 0.5em; +} + +.large { + font-size: 1.3em; +} + +.medium { + font-size: 1.1em; +} + +.small { + font-size: 0.9em; +} + +.pull-left-narrow { + float: left; + width: 35%; +} + +.pull-right-wide { + float: right; + width: 60%; +} + +.highlight-box { + background-color: #fff3cd; + border-left: 5px solid #ffc107; + padding: 15px; + margin: 15px 0; +} + +.key-point { + background-color: #e3f2fd; + border-left: 5px solid #2196f3; + padding: 15px; + margin: 15px 0; + font-weight: 500; +} + +.methodology { + background-color: #f5f5f5; + padding: 15px; + border-radius: 5px; + margin: 10px 0; +} + +.timeline-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 15px; + margin: 20px 0; +} + +.timeline-item { + padding: 15px; + border-radius: 5px; + border-left: 4px solid; + font-size: 0.85em; +} + +.timeline-item strong { + display: block; + font-size: 1.3em; + margin-bottom: 5px; +} + +.pre-birth { + background-color: #e3f2fd; + border-color: #2196f3; +} + +.digressions { + background-color: #fff9e6; + border-color: #ffc107; +} + +.life { + background-color: #e8f5e9; + border-color: #4caf50; +} + +.present { + background-color: #f3e5f5; + border-color: #9c27b0; +} +``` + + + +# The Problem of Narrative Time: + +.large[ +Tristram Shandy attempts to tell his life story but **never arrives at the present** +] + +-- + +.highlight-box[ +By Volume 4, Tristram has covered only **one day** of his life + +Yet the novel spans **9 volumes** and **~300 chapters** +] + +-- + +.medium[ +**The central question:** +How does Sterne's temporal disorder function as narrative structure? +] + +??? +Speaker notes: +- Published 1759-1767, revolutionary experimental novel +- Narrator constantly digresses backward and forward in time +- Not random chaos—there's a pattern we can computationally model +- This talk shows how data visualization makes temporal structure visible + +--- + +# Why Linear Chronology Fails + +.pull-left-narrow[ +### Traditional Biography +- Birth → Childhood → Youth → Death +- Chronological progression +- Linear narrative arc +] + +.pull-right-wide[ +### Tristram Shandy +- Begins with conception... +- ...then jumps to Uncle Toby's war injury **decades earlier** +- ...then to Yorick's death **years before birth** +- ...then to present-day writing +- ...then back to childhood accidents +- **No sustained chronological sequence** +] + +-- + +.key-point[ +**Sterne's structure is not broken—it's deliberate** +Digression becomes the primary narrative mode +] + +??? +Speaker notes: +- Traditional autobiography: Franklin, Rousseau follow chronological order +- Sterne deliberately frustrates this expectation +- The digressions ARE the point—not obstacles to the "real" story +- Question: Can we map this disorder systematically? + +--- + +# Data Collection & Web Scraping Pipeline + +.methodology[ +**Source:** Project Gutenberg full text (HTML) +**Method:** Python web scraping with BeautifulSoup +] + +-- + +### Pipeline Architecture + +1. **Chapter Extraction** + - Regex pattern matching: `C H A P. [Roman numeral]` + - 300+ chapters across 9 volumes + - Full text preservation + +-- + +2. **Intelligent Quotation Selection** + - Temporal markers & character names + - Keyword matching to narrative period + +--- +# Narrative Classification System + +3\. **Narrative Classification** + - 18 time period categories + - Chronological position (-4 to 11) + +??? +Speaker notes: +- Not manual coding—fully automated and reproducible +- Quotation algorithm selects contextually relevant excerpts +- Each chapter gets chronological position based on story time +- Output: structured JSON with 300 annotated chapters + +--- + +# Modeling Time: The -4 to 11 Scale + +.timeline-grid[ +.timeline-item.pre-birth[ +**-4 to -1** +Before Birth +Family history, Namur, Yorick +] + +.timeline-item.digressions[ +**0** +Digressions +Timeless embedded narratives +] + +.timeline-item.life[ +**1 to 8** +Tristram's Life +Conception → Youth +] + +.timeline-item.present[ +**10-11** +Present +Tristram writing, meta-commentary +] +] + +-- + +.highlight-box[ +**Key Innovation:** Negative numbers represent *years before birth* + +Position -2 = Uncle Toby wounded at Namur (decades before Tristram exists) +Position 0 = Digressions outside story time +Position 10 = Present-day narration +] + +-- + +.medium[ +**This allows us to plot:** Reading Order (1–300) vs. Chronological Position (-4 to 11) +] + +??? +Speaker notes: +- Not arbitrary—based on actual temporal relationships in text +- Negative scale shows how far back Sterne reaches +- Zero marks timeless philosophical asides +- Positive numbers follow life chronology +- Creates two-dimensional temporal map + +--- + +# Visualization Architecture + +### Dual-Panel Design + +.pull-left[ +**Top Panel: Narrative Path** +- X-axis: Reading order (1→300) +- Y-axis: Chronological position +- Connected line = reader's path through time +- Color-coded by narrative period +] + +.pull-right[ +**Bottom Panel: Distribution** +- Shows density at each position +- Reveals where novel "dwells" +- Stacked bars by period +] + +-- + +.key-point[ +**If the novel were linear:** smooth diagonal line +**What we actually see:** wild temporal oscillation +] + +??? +Speaker notes: +- Two complementary views of same data +- Top: shows temporal MOVEMENT (reader's journey) +- Bottom: shows temporal DENSITY (where Sterne lingers) +- Not decorative—reveals structure of delay +- Every vertical jump = temporal disruption + +--- +class: center, middle + +# Interactive Narrative Model + +```{r, echo=FALSE, out.width="100%"} +knitr::include_graphics("newplot.png") +``` + +.footnote[ +Dual-panel temporal architecture visualization +] + +??? +Speaker notes: +- This is the actual working model +- Notice the jagged line—not smooth progression +- Hover over points to see quotations from chapters +- Bottom panel shows Sterne spends ~80 chapters at position 10 (present) +- But spreads life events thinly across positive positions +- Pattern visible: oscillation between past/present/digression + +--- + +# Key Insights from the Model + +### Quantified Temporal Disruption + +-- + +1. **Oscillation Pattern** + - Novel jumps constantly: past (-4) → present (10) → digression (0) + - No sustained chronological progression + - Average jump magnitude: **6 chronological positions** + +-- + +2. **Narrative Density** + - **~80 chapters** at position 10 (present narration) + - Only **~40 chapters** cover positions 1-8 (entire life) + - **Present overwhelms chronology** by 2:1 ratio + +-- + +3. **Structured Delay** + - Toby's story split across positions -2, -1, and 7 + - Birth delayed until reading order ~60 + - Childhood scattered non-consecutively + +-- + +.highlight-box[ +**Finding:** Digressions are not chaos—they form a **temporal architecture** +] + +??? +Speaker notes: +- These are empirical findings from the data +- Not impressionistic—computationally measured +- The 2:1 ratio quantifies the "problem" of Tristram Shandy +- More time spent commenting than narrating +- This is the architecture of delay made visible + +--- + +# What This Reveals About Sterne + +.large[ +**Thesis:** Sterne's digressions are **structural**, not ornamental +] + +-- + +### Three Interpretive Claims: + +-- + +1. **Memory operates non-linearly** + - Flashbacks interrupt forward progress + - Past intrudes on present constantly + - The visualization **models memory itself** + +-- + +2. **Narration is impossible** + - To tell one story requires telling ten others + - Each explanation demands further backstory + - The act of writing **delays the story indefinitely** + +-- + +3. **Form enacts argument** + - Structure performs the impossibility of autobiography + +??? +Speaker notes: +- Not just showing disorder—arguing disorder is meaningful +- Visualization proves form and content are inseparable +- Sterne demonstrates autobiography is always incomplete +- Each digression reveals narrative as construct +- Modern readers frustrated by this—18th century readers delighted + +--- + +# Conclusion + +.pull-left[ +### What I Built +- Web scraping pipeline +- 300-chapter dataset +- Chronological coding +- Interactive visualization +- Hoverable quotations +] + +.pull-right[ +### What I Learned +- Temporal disorder is **systematic** +- Digressions form **architecture** +- Visualization makes **structure visible** +- Computational methods reveal **literary patterns** +] + +-- + +.key-point.large[ +**Core finding:** +Tristram Shandy's "chaos" is actually a **quantifiable oscillation** +The jagged line reveals the **structure of delay** +] + +??? +Speaker notes: +- Started with question: how does temporal disorder work? +- Answer: it's patterned, measurable, visualizable +- DH methods don't replace close reading—they enable new close reading +- We can now SEE the structure Sterne built +- This approach applies to any non-linear narrative +- Questions? + +--- +class: center, middle + +# Thank You + +.large[ +**Questions?** +] + +.medium[ +David Whitmer +Dickinson College +Data Capstone 400 +] + +.center[ +.small[ +Project code and visualization available upon request +Source: Sterne, Laurence. *The Life and Opinions of Tristram Shandy, Gentleman*. Project Gutenberg. +] +] + +??? +Speaker notes: +- Open to questions about methodology, findings, or technical implementation +- Happy to discuss extensions: other non-linear novels, comparative analysis +- All code is reproducible and available diff --git a/tristram_shandy_complete_analysis.html b/tristram_shandy_complete_analysis.html new file mode 100644 index 0000000..75f42c7 --- /dev/null +++ b/tristram_shandy_complete_analysis.html @@ -0,0 +1,380 @@ + + + + + Tristram Shandy: Temporal Architecture Analysis + + + + +
+

THE LIFE AND OPINIONS OF TRISTRAM SHANDY, GENTLEMAN

+

by Laurence Sterne (1759-1767)

+

+ A Digital Humanities Analysis of Non-Linear Narrative Structure +

+
+ +
+

About the Novel

+

+ The Life and Opinions of Tristram Shandy, Gentleman is one of literature's + first experimental novels and a masterpiece of non-linear storytelling. The narrator, + Tristram, attempts to tell his life story but constantly digresses into tangents, + backstories, and philosophical musings. The result is a chaotic, playful narrative + that jumps backward and forward in time. +

+
+ 💡 Key Insight: Tristram famously doesn't even get to his own birth + until Volume 3 — he's too busy telling us about everything that led up to it! The + novel's structure itself becomes an argument about memory, digression, and the + impossibility of linear self-narration. +
+
+ +
+

The Dual-Panel Visualization Explained

+

+ This is not a generic timeline. It is a temporal architecture visualization + designed to make the novel's non-chronological structure legible. The visualization consists + of two synchronized panels: +

+ +

Top Panel: Narrative Path Timeline

+ +

+ If the novel were linear, the line would ascend steadily from bottom-left + to top-right. Instead, you see dramatic vertical jumps—Sterne constantly yanking the reader + backward and forward through time. +

+ +

Bottom Panel: Chronological Distribution

+

+ A stacked bar chart showing how many chapters occupy each chronological position. This reveals + where the novel dwells chronologically—notice how much time Sterne spends on "present + narration" (position 10) compared to actual life events. +

+
+ +
+

Understanding the Chronological Scale

+ +
+
+ -4 to -1 +
Before Tristram's Birth
+ Family history, Uncle Toby's war injury at Namur, Parson Yorick's life and death +
+ +
+ 0 +
Digressions
+ Timeless stories-within-stories, philosophical theories, embedded tales like Slawkenbergius +
+ +
+ 1 to 8 +
Tristram's Life
+ Conception → Birth → Christening → Childhood → Youth (chronological progression) +
+ +
+ 10-11 +
Present Narrative
+ Tristram writing the book, meta-commentary on the act of storytelling itself +
+
+ +

Why Negative Numbers?

+

+ Tristram doesn't start his story at his birth — he goes backward to tell us about + his family's history first. Uncle Toby's war wound at the Siege of Namur happened years + before Tristram was conceived, so it gets a negative chronological position (-2). + Think of position 0 as Tristram's birth (the origin point), with negative numbers representing + the years before that event. This shows how far back in time the narrative reaches before + the protagonist even exists. +

+
+ +
+

What the Visualization Reveals

+ +
+ 💡 Literary Significance: The visualization quantifies narrative disorder. + A linear biography would show a smooth upward slope. Sterne's jagged line—with its wild + oscillations between past, present, and digression—reveals a narrator unable or unwilling + to tell his story in chronological order. The structure itself becomes an argument + about memory, digression, and the impossibility of linear self-narration. +
+
+ +
+

Interactive Temporal Architecture

+

+ How to interact: Hover over any point to see chapter details and representative + quotations. Click and drag to zoom into specific regions. Double-click to reset the view. + The line connects chapters in reading order—watch how it oscillates through time. +

+
+
+ +
+

Methodology

+ +
+

Data Collection & Processing

+

Source: Full text from Project Gutenberg

+

Chapters Analyzed: 300

+

Quotation Selection: Automated intelligent extraction based on:

+ +
+ +

Dataset Summary

+

Total chapters analyzed: 300

+

Reading order range: 1 to 300

+

Chronological range: -4 to 11

+ +

Narrative Period Distribution

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Narrative PeriodNumber of ChaptersChronological Position
Toby Recovery87-1
Namur55-2
Conception461
Tristram Writing4310
Birth242
Toby & Wadman127
Parson Yorick's life11-3
Childhood Accidents84
Slawkenbergius40
Meta-commentary411
Walter's Theories30
Aunt Dinah's scandal2-4
Sermon10
+
+ +
+

+ Citation: Sterne, Laurence. The Life and Opinions of Tristram Shandy, + Gentleman. Project Gutenberg. + + https://www.gutenberg.org/ebooks/1079 + +

+
+ + + + diff --git a/~$ni Project Proposal Idea 2.docx b/~$ni Project Proposal Idea 2.docx new file mode 100644 index 0000000..faac151 Binary files /dev/null and b/~$ni Project Proposal Idea 2.docx differ