fix(audiobook): add AudiobookPlan.chapter_count so import doesn't 500 (#544)
POST /audiobook/import ended with `plan.chapter_count`, but AudiobookPlan only exposed `char_count` (a property) and emitted `chapter_count` from `to_dict()` — so the attribute access raised AttributeError, surfacing as "500 Internal Server Error: 'AudiobookPlan' object has no attribute 'chapter_count'". The parse itself succeeded, so this hit every import format (.txt/.md/.epub/.pdf), not just PDF. Add a `chapter_count` property mirroring `char_count`, and have `to_dict()` derive its key from it so the attribute and serialized key can't drift. No API/schema/data change. Tests: unit property test + a direct-handler /audiobook/import regression (pdf/md/txt) that fails-before with the AttributeError. Fixes #543 Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
mergetest
Claude Opus 4.8
parent
6dccf0390d
commit
7678d4cca3
@@ -65,10 +65,14 @@ class AudiobookPlan:
|
||||
def char_count(self) -> int:
|
||||
return sum(c.char_count for c in self.chapters)
|
||||
|
||||
@property
|
||||
def chapter_count(self) -> int:
|
||||
return len(self.chapters)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"chapters": [c.to_dict() for c in self.chapters],
|
||||
"chapter_count": len(self.chapters),
|
||||
"chapter_count": self.chapter_count,
|
||||
"char_count": self.char_count,
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,15 @@ def test_plan_to_dict_shape():
|
||||
assert d["chapters"][0]["spans"][0]["text"] == "Hi there."
|
||||
|
||||
|
||||
def test_plan_chapter_count_property():
|
||||
# Regression for #543: the /audiobook/import endpoint reads plan.chapter_count
|
||||
# directly (not via to_dict), so the attribute must exist and stay in lockstep
|
||||
# with the serialized key.
|
||||
plan = parse_audiobook_script("# A\nHi.\n# B\nBye.")
|
||||
assert plan.chapter_count == 2
|
||||
assert plan.chapter_count == plan.to_dict()["chapter_count"]
|
||||
|
||||
|
||||
# ── FFMETADATA ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_ffmetadata_cumulative_offsets():
|
||||
|
||||
@@ -5,10 +5,12 @@ EPUB zip in memory (no fixture file, no new dep).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
from fastapi import UploadFile
|
||||
|
||||
from services.longform_import import (
|
||||
chapterize_plaintext,
|
||||
@@ -16,6 +18,7 @@ from services.longform_import import (
|
||||
pdf_to_chapter_script,
|
||||
)
|
||||
from services.audiobook import parse_audiobook_script
|
||||
from api.routers.audiobook import audiobook_import
|
||||
|
||||
|
||||
# ── PDF fixture builder ───────────────────────────────────────────────────
|
||||
@@ -219,3 +222,28 @@ def test_pdf_too_many_pages_guard():
|
||||
data = _make_pdf(["Chapter 1", "Hi."])
|
||||
with pytest.raises(ValueError, match="too many pages"):
|
||||
pdf_to_chapter_script(data, max_pages=0)
|
||||
|
||||
|
||||
# ── import endpoint ────────────────────────────────────────────────────────
|
||||
# Calls the handler directly (no TestClient → no main+torch import), mirroring
|
||||
# tests/test_audiobook_cover.py.
|
||||
|
||||
def _upload(name: str, data: bytes) -> UploadFile:
|
||||
return UploadFile(io.BytesIO(data), filename=name)
|
||||
|
||||
|
||||
def test_import_endpoint_returns_chapter_count():
|
||||
# Regression for #543: parsing succeeded but the endpoint then read
|
||||
# plan.chapter_count, which didn't exist → 500 AttributeError. Covers the
|
||||
# whole class — every import format hits this same return path.
|
||||
pdf = _make_pdf(["Chapter 1", "Once upon a time.", "Chapter 2", "The end."])
|
||||
for name, data in [
|
||||
("book.pdf", pdf),
|
||||
("book.md", b"# One\nhello\n\n# Two\nworld"),
|
||||
("book.txt", b"just a flat blob of narration with no headings"),
|
||||
]:
|
||||
res = asyncio.run(audiobook_import(_upload(name, data)))
|
||||
assert isinstance(res["chapters"], int) and res["chapters"] >= 1
|
||||
assert res["text"].strip()
|
||||
# The two-chapter inputs parse to exactly two chapters.
|
||||
assert asyncio.run(audiobook_import(_upload("book.pdf", pdf)))["chapters"] == 2
|
||||
|
||||
Reference in New Issue
Block a user