Skip to content

Commit f06b508

Browse files
s0h3ylAssemblyAI
andauthored
feat: add auto_chapters functionality (#14)
Co-authored-by: AssemblyAI <[email protected]>
1 parent b9d9e38 commit f06b508

File tree

6 files changed

+230
-49
lines changed

6 files changed

+230
-49
lines changed

README.md

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ With a single API call, get access to AI models built on the latest AI breakthro
2323
- [Example](#examples)
2424
- [Core Examples](#core-examples)
2525
- [LeMUR Examples](#lemur-examples)
26-
- [Audio Intelligence+ Examples](#audio-intelligence-examples)
26+
- [Audio Intelligence Examples](#audio-intelligence-examples)
2727
- [Playgrounds](#playgrounds)
2828
- [Advanced](#advanced-todo)
2929

@@ -159,35 +159,6 @@ print(transcript.text)
159159

160160
</details>
161161

162-
<details>
163-
<summary>Summarize the content of a transcript</summary>
164-
165-
```python
166-
import assemblyai as aai
167-
168-
transcriber = aai.Transcriber()
169-
transcript = transcriber.transcribe(
170-
"https://example.org/audio.mp3",
171-
config=aai.TranscriptionConfig(summarize=True)
172-
)
173-
174-
print(transcript.summary)
175-
```
176-
177-
By default, the summarization model will be `informative` and the summarization type will be `bullets`. [Read more about summarization models and types here](https://www.assemblyai.com/docs/Models/summarization#types-and-models).
178-
179-
To change the model and/or type, pass additional parameters to the `TranscriptionConfig`:
180-
181-
```python
182-
config=aai.TranscriptionConfig(
183-
summarize=True,
184-
summary_model=aai.SummarizationModel.catchy,
185-
summary_type=aai.Summarizationtype.headline
186-
)
187-
```
188-
189-
</details>
190-
191162
---
192163

193164
### **LeMUR Examples**
@@ -260,7 +231,7 @@ for result in result:
260231

261232
---
262233

263-
### **Audio Intelligence+ Examples**
234+
### **Audio Intelligence Examples**
264235

265236
<details>
266237
<summary>PII Redact a Transcript</summary>
@@ -286,6 +257,57 @@ transcriber = aai.Transcriber()
286257
transcript = transcriber.transcribe("https://example.org/audio.mp3", config)
287258
```
288259

260+
</details>
261+
<details>
262+
<summary>Summarize the content of a transcript over time</summary>
263+
264+
```python
265+
import assemblyai as aai
266+
267+
transcriber = aai.Transcriber()
268+
transcript = transcriber.transcribe(
269+
"https://example.org/audio.mp3",
270+
config=aai.TranscriptionConfig(auto_chapters=True)
271+
)
272+
273+
for chapter in transcript.chapters:
274+
print(f"Summary: {chapter.summary}") # A one paragraph summary of the content spoken during this timeframe
275+
print(f"Start: {chapter.start}, End: {chapter.end}") # Timestamps (in milliseconds) of the chapter
276+
print(f"Healine: {chapter.headline}") # A single sentence summary of the content spoken during this timeframe
277+
print(f"Gist: {chapter.gist}") # An ultra-short summary, just a few words, of the content spoken during this timeframe
278+
```
279+
280+
[Read more about auto chapters here.](https://www.assemblyai.com/docs/Models/auto_chapters)
281+
282+
</details>
283+
284+
<details>
285+
<summary>Summarize the content of a transcript</summary>
286+
287+
```python
288+
import assemblyai as aai
289+
290+
transcriber = aai.Transcriber()
291+
transcript = transcriber.transcribe(
292+
"https://example.org/audio.mp3",
293+
config=aai.TranscriptionConfig(summarization=True)
294+
)
295+
296+
print(transcript.summary)
297+
```
298+
299+
By default, the summarization model will be `informative` and the summarization type will be `bullets`. [Read more about summarization models and types here](https://www.assemblyai.com/docs/Models/summarization#types-and-models).
300+
301+
To change the model and/or type, pass additional parameters to the `TranscriptionConfig`:
302+
303+
```python
304+
config=aai.TranscriptionConfig(
305+
summarization=True,
306+
summary_model=aai.SummarizationModel.catchy,
307+
summary_type=aai.SummarizationType.headline
308+
)
309+
```
310+
289311
</details>
290312

291313
---
@@ -297,7 +319,6 @@ Visit one of our Playgrounds:
297319
- [LeMUR Playground](https://www.assemblyai.com/playground/v2/source)
298320
- [Transcription Playground](https://www.assemblyai.com/playground)
299321

300-
301322
# Advanced
302323

303324
## How the SDK handles Default Configurations
@@ -329,7 +350,6 @@ transcriber = aai.Transcriber()
329350
transcriber.config = aai.TranscriptionConfig(punctuate=False, format_text=False)
330351
```
331352

332-
333353
In case you want to override the `Transcriber`'s configuration for a specific operation with a different one, you can do so via the `config` parameter of a `.transcribe*(...)` method:
334354

335355
```python

assemblyai/transcriber.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ def summary(self) -> Optional[str]:
210210

211211
return self._impl.transcript.summary
212212

213+
@property
214+
def chapters(self) -> Optional[List[types.Chapter]]:
215+
return self._impl.transcript.chapters
216+
213217
@property
214218
def status(self) -> types.TranscriptStatus:
215219
"The current status of the transcript"

assemblyai/types.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,8 @@ class RawTranscriptionConfig(BaseModel):
354354
# sentiment_analysis: bool = False
355355
# "Enable Sentiment Analysis."
356356

357-
# auto_chapters: bool = False
358-
# "Enable Auto Chapters."
357+
auto_chapters: Optional[bool]
358+
"Enable Auto Chapters."
359359

360360
# entity_detection: bool = False
361361
# "Enable Entity Detection."
@@ -415,7 +415,7 @@ def __init__(
415415
custom_spelling: Optional[Dict[str, Union[str, Sequence[str]]]] = None,
416416
disfluencies: Optional[bool] = None,
417417
# sentiment_analysis: bool = False,
418-
# auto_chapters: bool = False,
418+
auto_chapters: Optional[bool] = None,
419419
# entity_detection: bool = False,
420420
summarization: Optional[bool] = None,
421421
summary_model: Optional[SummarizationModel] = None,
@@ -491,7 +491,7 @@ def __init__(
491491
self.set_custom_spelling(custom_spelling, override=True)
492492
self.disfluencies = disfluencies
493493
# self.sentiment_analysis = sentiment_analysis
494-
# self.auto_chapters = auto_chapters
494+
self.auto_chapters = auto_chapters
495495
# self.entity_detection = entity_detection
496496
self.set_summarize(
497497
summarization,
@@ -707,17 +707,23 @@ def disfluencies(self, enable: Optional[bool]) -> None:
707707

708708
# self._raw_transcription_config.sentiment_analysis = enable
709709

710-
# @property
711-
# def auto_chapters(self) -> bool:
712-
# "Returns the status of the Auto Chapters feature."
710+
@property
711+
def auto_chapters(self) -> bool:
712+
"Returns the status of the Auto Chapters feature."
713+
714+
return self._raw_transcription_config.auto_chapters
713715

714-
# return self._raw_transcription_config.auto_chapters
716+
@auto_chapters.setter
717+
def auto_chapters(self, enable: bool) -> None:
718+
"Enable Auto Chapters."
715719

716-
# @auto_chapters.setter
717-
# def auto_chapters(self, enable: bool) -> None:
718-
# "Enable Auto Chapters."
720+
# Validate required params are also set
721+
if self.punctuate == False:
722+
raise ValueError(
723+
"If `auto_chapters` is enabled, then `punctuate` must not be disabled"
724+
)
719725

720-
# self._raw_transcription_config.auto_chapters = enable
726+
self._raw_transcription_config.auto_chapters = enable
721727

722728
# @property
723729
# def entity_detection(self) -> bool:
@@ -1317,8 +1323,8 @@ class BaseTranscript(BaseModel):
13171323
# sentiment_analysis: bool = False
13181324
# "Enable Sentiment Analysis."
13191325

1320-
# auto_chapters: bool = False
1321-
# "Enable Auto Chapters."
1326+
auto_chapters: Optional[bool]
1327+
"Enable Auto Chapters."
13221328

13231329
# entity_detection: bool = False
13241330
# "Enable Entity Detection."
@@ -1401,7 +1407,7 @@ class TranscriptResponse(BaseTranscript):
14011407
# iab_categories_result: Optional[IABResponse] = None
14021408
# "The list of results when Topic Detection is enabled"
14031409

1404-
# chapters: Optional[List[Chapter]] = None
1410+
chapters: Optional[List[Chapter]]
14051411
# "When Auto Chapters is enabled, the list of Auto Chapters results"
14061412

14071413
# sentiment_analysis_results: Optional[List[Sentiment]] = None

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setup(
99
name="assemblyai",
10-
version="0.6.0",
10+
version="0.7.0",
1111
description="AssemblyAI Python SDK",
1212
author="AssemblyAI",
1313
author_email="[email protected]",

tests/unit/factories.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ class Meta:
3737
words = factory.List([factory.SubFactory(UtteranceWordFactory)])
3838

3939

40+
class ChapterFactory(factory.Factory):
41+
class Meta:
42+
model = types.Chapter
43+
44+
summary = factory.Faker("sentence")
45+
headline = factory.Faker("sentence")
46+
gist = factory.Faker("sentence")
47+
start = factory.Faker("pyint")
48+
end = factory.Faker("pyint")
49+
50+
4051
class BaseTranscriptFactory(factory.Factory):
4152
class Meta:
4253
model = types.BaseTranscript

tests/unit/test_auto_chapters.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import json
2+
from typing import Any, Dict, Tuple
3+
4+
import factory
5+
import httpx
6+
import pytest
7+
from pytest_httpx import HTTPXMock
8+
9+
import assemblyai as aai
10+
from tests.unit import factories
11+
12+
aai.settings.api_key = "test"
13+
14+
15+
class AutoChaptersResponseFactory(factories.TranscriptCompletedResponseFactory):
16+
chapters = factory.List([factory.SubFactory(factories.ChapterFactory)])
17+
18+
19+
def __submit_mock_request(
20+
httpx_mock: HTTPXMock,
21+
mock_response: Dict[str, Any],
22+
config: aai.TranscriptionConfig,
23+
) -> Tuple[Dict[str, Any], aai.Transcript]:
24+
"""
25+
Helper function to abstract mock transcriber calls with given `TranscriptionConfig`,
26+
and perform some common assertions.
27+
"""
28+
29+
mock_transcript_id = mock_response.get("id", "mock_id")
30+
31+
# Mock initial submission response (transcript is processing)
32+
mock_processing_response = factories.generate_dict_factory(
33+
factories.TranscriptProcessingResponseFactory
34+
)()
35+
36+
httpx_mock.add_response(
37+
url=f"{aai.settings.base_url}/transcript",
38+
status_code=httpx.codes.OK,
39+
method="POST",
40+
json={
41+
**mock_processing_response,
42+
"id": mock_transcript_id, # inject ID from main mock response
43+
},
44+
)
45+
46+
# Mock polling-for-completeness response, with completed transcript
47+
httpx_mock.add_response(
48+
url=f"{aai.settings.base_url}/transcript/{mock_transcript_id}",
49+
status_code=httpx.codes.OK,
50+
method="GET",
51+
json=mock_response,
52+
)
53+
54+
# == Make API request via SDK ==
55+
transcript = aai.Transcriber().transcribe(
56+
data="https://example.org/audio.wav",
57+
config=config,
58+
)
59+
60+
# Check that submission and polling requests were made
61+
assert len(httpx_mock.get_requests()) == 2
62+
63+
# Extract body of initial submission request
64+
request = httpx_mock.get_requests()[0]
65+
request_body = json.loads(request.content.decode())
66+
67+
return request_body, transcript
68+
69+
70+
def test_auto_chapters_fails_without_punctuation(httpx_mock: HTTPXMock):
71+
"""
72+
Tests whether the SDK raises an error before making a request
73+
if `auto_chapters` is enabled and `punctuation` is disabled
74+
"""
75+
76+
with pytest.raises(ValueError) as error:
77+
__submit_mock_request(
78+
httpx_mock,
79+
mock_response={}, # response doesn't matter, since it shouldn't occur
80+
config=aai.TranscriptionConfig(
81+
auto_chapters=True,
82+
punctuate=False,
83+
),
84+
)
85+
# Check that the error message informs the user of the invalid parameter
86+
assert "punctuate" in str(error)
87+
88+
# Check that the error was raised before any requests were made
89+
assert len(httpx_mock.get_requests()) == 0
90+
91+
# Inform httpx_mock that it's okay we didn't make any requests
92+
httpx_mock.reset(False)
93+
94+
95+
def test_auto_chapters_disabled_by_default(httpx_mock: HTTPXMock):
96+
"""
97+
Tests that excluding `auto_chapters` from the `TranscriptionConfig` will
98+
result in the default behavior of it being excluded from the request body
99+
"""
100+
request_body, transcript = __submit_mock_request(
101+
httpx_mock,
102+
mock_response=factories.generate_dict_factory(
103+
factories.TranscriptCompletedResponseFactory
104+
)(),
105+
config=aai.TranscriptionConfig(),
106+
)
107+
assert request_body.get("auto_chapters") is None
108+
assert transcript.chapters is None
109+
110+
111+
def test_auto_chapters_enabled(httpx_mock: HTTPXMock):
112+
"""
113+
Tests that including `auto_chapters=True` in the `TranscriptionConfig`
114+
will result in `auto_chapters=True` in the request body, and that the
115+
response is properly parsed into a `Transcript` object
116+
"""
117+
mock_response = factories.generate_dict_factory(AutoChaptersResponseFactory)()
118+
request_body, transcript = __submit_mock_request(
119+
httpx_mock,
120+
mock_response=mock_response,
121+
config=aai.TranscriptionConfig(auto_chapters=True),
122+
)
123+
124+
# Check that request body was properly defined
125+
assert request_body.get("auto_chapters") == True
126+
127+
# Check that transcript was properly parsed from JSON response
128+
assert transcript.error is None
129+
assert transcript.chapters is not None
130+
assert len(transcript.chapters) > 0
131+
assert len(transcript.chapters) == len(mock_response["chapters"])
132+
133+
for response_chapter, transcript_chapter in zip(
134+
mock_response["chapters"], transcript.chapters
135+
):
136+
assert transcript_chapter.summary == response_chapter["summary"]
137+
assert transcript_chapter.headline == response_chapter["headline"]
138+
assert transcript_chapter.gist == response_chapter["gist"]
139+
assert transcript_chapter.start == response_chapter["start"]
140+
assert transcript_chapter.end == response_chapter["end"]

0 commit comments

Comments
 (0)