Created
June 27, 2026 17:35
-
-
Save doegox/0710704d2ba2fb93da999f1aaa0ff6b2 to your computer and use it in GitHub Desktop.
Google Slides speaker notes => export as ODP => pdfpc JSON
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import sys | |
| import xml.etree.ElementTree as ET | |
| import json | |
| import os | |
| import zipfile | |
| import tempfile | |
| ns = { | |
| 'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0', | |
| 'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0', | |
| 'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0', | |
| } | |
| def extract_notes(content_xml_path): | |
| tree = ET.parse(content_xml_path) | |
| root = tree.getroot() | |
| pages = root.findall('.//draw:page', ns) | |
| notes = [] | |
| for i, page in enumerate(pages): | |
| notes_elem = page.find('.//presentation:notes', ns) | |
| note_text = '' | |
| if notes_elem is not None: | |
| lines = [] | |
| for tp in notes_elem.findall('.//text:p', ns): | |
| parts = [] | |
| if tp.text: | |
| parts.append(tp.text) | |
| for child in tp: | |
| tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag | |
| if tag == 'line-break': | |
| parts.append('\n') | |
| elif tag == 'tab': | |
| parts.append('\t') | |
| elif tag == 's': | |
| count = child.get('{urn:oasis:names:tc:opendocument:xmlns:text:1.0}c', '1') | |
| parts.append(' ' * int(count)) | |
| if child.text: | |
| parts.append(child.text) | |
| if child.tail: | |
| parts.append(child.tail) | |
| lines.append(''.join(parts)) | |
| note_text = '\n'.join(lines) | |
| notes.append({'idx': i, 'note': note_text}) | |
| return notes | |
| def main(): | |
| if len(sys.argv) != 2: | |
| print(f'Usage: {sys.argv[0]} <file.odp or extracted-dir>', file=sys.stderr) | |
| sys.exit(1) | |
| path = sys.argv[1] | |
| if os.path.isdir(path): | |
| content_xml = os.path.join(path, 'content.xml') | |
| base = path.rstrip('/') | |
| elif zipfile.is_zipfile(path): | |
| tmpdir = tempfile.mkdtemp() | |
| with zipfile.ZipFile(path) as zf: | |
| zf.extract('content.xml', tmpdir) | |
| content_xml = os.path.join(tmpdir, 'content.xml') | |
| base = path | |
| else: | |
| print(f'Error: {path} is not an ODP file or extracted directory', file=sys.stderr) | |
| sys.exit(1) | |
| notes = extract_notes(content_xml) | |
| pdfpc = { | |
| 'pdfpcFormat': 2, | |
| 'pages': notes, | |
| } | |
| outpath = base + '.pdfpc' | |
| with open(outpath, 'w', encoding='utf-8') as f: | |
| json.dump(pdfpc, f, indent=2, ensure_ascii=False) | |
| non_empty = sum(1 for n in notes if n['note'].strip()) | |
| print(f'{outpath}: {len(notes)} slides, {non_empty} with notes') | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment