notionary 0.1.10__py3-none-any.whl → 0.1.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/core/converters/elements/audio_element.py +141 -0
- notionary/core/converters/registry/block_element_registry.py +1 -5
- notionary/core/converters/registry/block_element_registry_builder.py +2 -0
- notionary/core/page/notion_page_manager.py +4 -13
- {notionary-0.1.10.dist-info → notionary-0.1.11.dist-info}/METADATA +1 -1
- {notionary-0.1.10.dist-info → notionary-0.1.11.dist-info}/RECORD +9 -8
- {notionary-0.1.10.dist-info → notionary-0.1.11.dist-info}/WHEEL +0 -0
- {notionary-0.1.10.dist-info → notionary-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.10.dist-info → notionary-0.1.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List
|
3
|
+
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
4
|
+
|
5
|
+
|
6
|
+
class AudioElement(NotionBlockElement):
|
7
|
+
"""
|
8
|
+
Handles conversion between Markdown audio embeds and Notion audio blocks.
|
9
|
+
|
10
|
+
Markdown audio syntax (custom format since standard Markdown doesn't support audio):
|
11
|
+
- $[Caption](https://example.com/audio.mp3) - Basic audio with caption
|
12
|
+
- $[](https://example.com/audio.mp3) - Audio without caption
|
13
|
+
- $[Caption](https://storage.googleapis.com/audio_summaries/example.mp3) - CDN hosted audio
|
14
|
+
|
15
|
+
Supports various audio URLs including direct audio file links from CDNs and other sources.
|
16
|
+
"""
|
17
|
+
|
18
|
+
# Regex pattern for audio syntax
|
19
|
+
PATTERN = re.compile(
|
20
|
+
r"^\$\[(.*?)\]" # $[Caption] part
|
21
|
+
+ r'\((https?://[^\s"]+)' # (URL part
|
22
|
+
+ r"\)$" # closing parenthesis
|
23
|
+
)
|
24
|
+
|
25
|
+
# Audio file extensions
|
26
|
+
AUDIO_EXTENSIONS = [".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac"]
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def match_markdown(text: str) -> bool:
|
30
|
+
"""Check if text is a markdown audio embed."""
|
31
|
+
text = text.strip()
|
32
|
+
return text.startswith("$[") and bool(AudioElement.PATTERN.match(text))
|
33
|
+
|
34
|
+
@staticmethod
|
35
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
36
|
+
"""Check if block is a Notion audio."""
|
37
|
+
return block.get("type") == "audio"
|
38
|
+
|
39
|
+
@staticmethod
|
40
|
+
def is_audio_url(url: str) -> bool:
|
41
|
+
"""Check if URL points to an audio file."""
|
42
|
+
return any(url.lower().endswith(ext) for ext in AudioElement.AUDIO_EXTENSIONS) or \
|
43
|
+
"audio" in url.lower() or \
|
44
|
+
"storage.googleapis.com/audio_summaries" in url.lower()
|
45
|
+
|
46
|
+
@staticmethod
|
47
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
48
|
+
"""Convert markdown audio embed to Notion audio block."""
|
49
|
+
audio_match = AudioElement.PATTERN.match(text.strip())
|
50
|
+
if not audio_match:
|
51
|
+
return None
|
52
|
+
|
53
|
+
caption = audio_match.group(1)
|
54
|
+
url = audio_match.group(2)
|
55
|
+
|
56
|
+
if not url:
|
57
|
+
return None
|
58
|
+
|
59
|
+
# Make sure this is an audio URL
|
60
|
+
if not AudioElement.is_audio_url(url):
|
61
|
+
# If not obviously an audio URL, we'll still accept it as the user might know better
|
62
|
+
pass
|
63
|
+
|
64
|
+
# Prepare the audio block
|
65
|
+
audio_block = {
|
66
|
+
"type": "audio",
|
67
|
+
"audio": {"type": "external", "external": {"url": url}},
|
68
|
+
}
|
69
|
+
|
70
|
+
# Add caption if provided
|
71
|
+
if caption:
|
72
|
+
audio_block["audio"]["caption"] = [
|
73
|
+
{"type": "text", "text": {"content": caption}}
|
74
|
+
]
|
75
|
+
|
76
|
+
return audio_block
|
77
|
+
|
78
|
+
@staticmethod
|
79
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
80
|
+
"""Convert Notion audio block to markdown audio embed."""
|
81
|
+
if block.get("type") != "audio":
|
82
|
+
return None
|
83
|
+
|
84
|
+
audio_data = block.get("audio", {})
|
85
|
+
|
86
|
+
# Handle both external and file (uploaded) audios
|
87
|
+
if audio_data.get("type") == "external":
|
88
|
+
url = audio_data.get("external", {}).get("url", "")
|
89
|
+
elif audio_data.get("type") == "file":
|
90
|
+
url = audio_data.get("file", {}).get("url", "")
|
91
|
+
else:
|
92
|
+
return None
|
93
|
+
|
94
|
+
if not url:
|
95
|
+
return None
|
96
|
+
|
97
|
+
# Extract caption if available
|
98
|
+
caption = ""
|
99
|
+
caption_rich_text = audio_data.get("caption", [])
|
100
|
+
if caption_rich_text:
|
101
|
+
caption = AudioElement._extract_text_content(caption_rich_text)
|
102
|
+
|
103
|
+
return f"$[{caption}]({url})"
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def is_multiline() -> bool:
|
107
|
+
"""Audio embeds are single-line elements."""
|
108
|
+
return False
|
109
|
+
|
110
|
+
@staticmethod
|
111
|
+
def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
|
112
|
+
"""Extract plain text content from Notion rich_text elements."""
|
113
|
+
result = ""
|
114
|
+
for text_obj in rich_text:
|
115
|
+
if text_obj.get("type") == "text":
|
116
|
+
result += text_obj.get("text", {}).get("content", "")
|
117
|
+
elif "plain_text" in text_obj:
|
118
|
+
result += text_obj.get("plain_text", "")
|
119
|
+
return result
|
120
|
+
|
121
|
+
@classmethod
|
122
|
+
def get_llm_prompt_content(cls) -> dict:
|
123
|
+
"""Returns information for LLM prompts about this element."""
|
124
|
+
return {
|
125
|
+
"description": "Embeds audio content from external sources like CDNs or direct audio URLs.",
|
126
|
+
"when_to_use": "Use audio embeds when you want to include audio content directly in your document. Audio embeds are useful for podcasts, music, voice recordings, or any content that benefits from audio explanation.",
|
127
|
+
"syntax": [
|
128
|
+
"$[](https://example.com/audio.mp3) - Audio without caption",
|
129
|
+
"$[Caption text](https://example.com/audio.mp3) - Audio with caption",
|
130
|
+
],
|
131
|
+
"supported_sources": [
|
132
|
+
"Direct links to audio files (.mp3, .wav, .ogg, etc.)",
|
133
|
+
"Google Cloud Storage links (storage.googleapis.com)",
|
134
|
+
"Other audio hosting platforms supported by Notion",
|
135
|
+
],
|
136
|
+
"examples": [
|
137
|
+
"$[Podcast Episode](https://storage.googleapis.com/audio_summaries/ep_ai_summary_127d02ec-ca12-4312-a5ed-cb14b185480c.mp3)",
|
138
|
+
"$[Voice recording](https://example.com/audio/recording.mp3)",
|
139
|
+
"$[](https://storage.googleapis.com/audio_summaries/example.mp3)",
|
140
|
+
],
|
141
|
+
}
|
@@ -233,8 +233,4 @@ paragraphs, lists, quotes, etc.
|
|
233
233
|
"""
|
234
234
|
element_docs = cls.generate_element_docs(element_classes)
|
235
235
|
|
236
|
-
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
# TODO: Testen ob der Inline Formatter hier überhaupt funktionieren tut:
|
236
|
+
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from typing import List, Type
|
2
2
|
from collections import OrderedDict
|
3
3
|
|
4
|
+
from notionary.core.converters.elements.audio_element import AudioElement
|
4
5
|
from notionary.core.converters.elements.embed_element import EmbedElement
|
5
6
|
from notionary.core.converters.elements.notion_block_element import NotionBlockElement
|
6
7
|
from notionary.core.converters.registry.block_element_registry import (
|
@@ -92,6 +93,7 @@ class BlockElementRegistryBuilder:
|
|
92
93
|
.add_element(ImageElement)
|
93
94
|
.add_element(VideoElement)
|
94
95
|
.add_element(EmbedElement)
|
96
|
+
.add_element(AudioElement)
|
95
97
|
.add_element(ParagraphElement)
|
96
98
|
) # Add paragraph last as fallback
|
97
99
|
|
@@ -311,20 +311,11 @@ async def demo2():
|
|
311
311
|
|
312
312
|
page_manager = NotionPageManager(url=url)
|
313
313
|
|
314
|
-
# Beispiel mit einem Embed-Element im Transcript-Toggle
|
315
314
|
markdown = """
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
+++ Transcript
|
321
|
-
<embed:Listen to this highlight>(https://share.snipd.com/snip/ad3d95b2-d648-4fa9-9036-bd7df653ea32)
|
322
|
-
<!-- spacer -->
|
323
|
-
... In diesem Teil des Podcasts erklärt der Sprecher, wie das Henneman-Größenprinzip funktioniert und wie Muskelfasern rekrutiert werden, basierend auf der Schwere des zu bewegenden Objekts.
|
324
|
-
"""
|
325
|
-
|
326
|
-
await page_manager.append_markdown(markdown)
|
327
|
-
print("Markdown wurde zur Notion-Seite hinzugefügt.")
|
315
|
+
$[Podcast Zusammenfassung](https://storage.googleapis.com/audio_summaries/ep_ai_summary_127d02ec-ca12-4312-a5ed-cb14b185480c.mp3)
|
316
|
+
"""
|
317
|
+
|
318
|
+
await page_manager.append_markdown(markdown=markdown)
|
328
319
|
|
329
320
|
if __name__ == "__main__":
|
330
321
|
asyncio.run(demo2())
|
@@ -3,6 +3,7 @@ notionary/core/notion_client.py,sha256=9o9-Ki1homkSbM1C51nsaAzVPMt2d4r8cPzoX3NK_
|
|
3
3
|
notionary/core/converters/__init__.py,sha256=GOUehJbe4BKHtec1MqL1YGu3AX8zFtkwSZfhYkY5-P0,1798
|
4
4
|
notionary/core/converters/markdown_to_notion_converter.py,sha256=PGtg4v5lUvkXXl1Y8E6a3Mf8hEfxfhBrslPs_H_Lq_E,16564
|
5
5
|
notionary/core/converters/notion_to_markdown_converter.py,sha256=c8GyWX8-UrNfRDk7OOBKbSEb5qOwljUCwI6g5risO2c,1287
|
6
|
+
notionary/core/converters/elements/audio_element.py,sha256=qTYxpErHw327JdESr4Biv-H89iZRXzBcllamrFu8P_k,5587
|
6
7
|
notionary/core/converters/elements/bookmark_element.py,sha256=bpHobkGnyBGDAJK5vY9R3Ntl4GiRSF-EyyA31aq2O3E,8593
|
7
8
|
notionary/core/converters/elements/callout_element.py,sha256=rkDoXikjIl-zU3GLawSXgRunBJGLnEvin9zIlCgW4TY,5964
|
8
9
|
notionary/core/converters/elements/code_block_element.py,sha256=G1iGMsGSK5KPSk-tA8TsPs9XNU9ydjYfOVnjIvdZG74,5189
|
@@ -20,14 +21,14 @@ notionary/core/converters/elements/text_inline_formatter.py,sha256=FE_Sq2cozpu5R
|
|
20
21
|
notionary/core/converters/elements/todo_lists.py,sha256=wgY6YejURBQ5ESdVLZVIy9QKchS-x8odrmS8X4cC5Kc,4265
|
21
22
|
notionary/core/converters/elements/toggle_element.py,sha256=Xv4MuuOyoamvT3IEJX4mynvLEycgtZ9LWt6Nm764KXE,6980
|
22
23
|
notionary/core/converters/elements/video_element.py,sha256=xrBLY3e_SgKNamItZkfPNMbNEh37Ftp4jWIV6nwV-ds,6047
|
23
|
-
notionary/core/converters/registry/block_element_registry.py,sha256=
|
24
|
-
notionary/core/converters/registry/block_element_registry_builder.py,sha256=
|
24
|
+
notionary/core/converters/registry/block_element_registry.py,sha256=SRIsPr6xb6xX7GHyrYYNBbt9-l6os29rQndmoXnr5W0,8707
|
25
|
+
notionary/core/converters/registry/block_element_registry_builder.py,sha256=D4GmIAdMoP7K1P78qlgN-GoDwtB_4dZTws049gtXQ5c,9457
|
25
26
|
notionary/core/database/database_info_service.py,sha256=58k7om0UXP8w0jCJHewccG5UbOvELMBAbQvXOm7F1OM,1341
|
26
27
|
notionary/core/database/notion_database_manager.py,sha256=cEQHf8bTcgA3hLMgsbMGSXhCmccmxWLQ6oOJiINR3ac,8257
|
27
28
|
notionary/core/database/notion_database_manager_factory.py,sha256=SoWUiM5zdajmR1ppYHTdPgHrdZbwuTMdoXW3_tBffyU,6831
|
28
29
|
notionary/core/database/notion_database_schema.py,sha256=WUIjG7I5kusk4GOOdmVSHIKc2Z8SeOgJ1FuGpTn4avQ,3304
|
29
30
|
notionary/core/database/models/page_result.py,sha256=Vmm5_oYpYAkIIJVoTd1ZZGloeC3cmFLMYP255mAmtaw,233
|
30
|
-
notionary/core/page/notion_page_manager.py,sha256=
|
31
|
+
notionary/core/page/notion_page_manager.py,sha256=O5GBa2KzjAiqH_NDg4JpJbkNcfh3AD5uzTkhqHMr8XE,13635
|
31
32
|
notionary/core/page/content/page_content_manager.py,sha256=GjgxVwi5NOMsvGwBNj-woq940ZupzALCQegDyIcsgMg,3358
|
32
33
|
notionary/core/page/metadata/metadata_editor.py,sha256=U3Ff9GRk28dqT9M1xsl6Q3Cj47-hB1n2pNJzeDXy4ks,4938
|
33
34
|
notionary/core/page/metadata/notion_icon_manager.py,sha256=v9pUG61TOT8x9UzDqBtQW6S5XQzWostq7IwrURnWvF4,1499
|
@@ -46,8 +47,8 @@ notionary/exceptions/page_creation_exception.py,sha256=4v7IuZD6GsQLrqhDLriGjuG3M
|
|
46
47
|
notionary/util/logging_mixin.py,sha256=fKsx9t90bwvL74ZX3dU-sXdC4TZCQyO6qU9I8txkw_U,1369
|
47
48
|
notionary/util/page_id_utils.py,sha256=9XexVGy8jY5iOlueS1MXFWHtRRmZ8js-EO3hT0_wg2E,1381
|
48
49
|
notionary/util/singleton_decorator.py,sha256=GTNMfIlVNRUVMw_c88xqd12-DcqZJjmyidN54yqiNVw,472
|
49
|
-
notionary-0.1.
|
50
|
-
notionary-0.1.
|
51
|
-
notionary-0.1.
|
52
|
-
notionary-0.1.
|
53
|
-
notionary-0.1.
|
50
|
+
notionary-0.1.11.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
|
51
|
+
notionary-0.1.11.dist-info/METADATA,sha256=3mCAD1Q1IlhUkPaf8NJAfDENEeZlZWjAmp1tN3e8F28,6154
|
52
|
+
notionary-0.1.11.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
53
|
+
notionary-0.1.11.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
|
54
|
+
notionary-0.1.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|