AuthorBot commited on
Commit
745bdc2
Β·
1 Parent(s): a806a0b

Fix ingest.py: use correct service signatures (chunk_document, summarize_book, file_path args)

Browse files
Files changed (1) hide show
  1. app/api/ingest.py +50 -35
app/api/ingest.py CHANGED
@@ -4,23 +4,21 @@ Routes (per implementation plan):
4
  POST /api/admin/{author_slug}/books/upload β€” Upload a book file
5
  GET /api/admin/{author_slug}/books/upload/progress β€” SSE ingestion progress
6
 
7
- RULE: Requires Author JWT (Bearer token).
8
- RULE: Magic byte validation before any processing.
9
  """
10
 
11
  import asyncio
12
  import json
 
 
 
13
 
14
  from fastapi import APIRouter, Depends, File, UploadFile, HTTPException
15
  from fastapi.responses import StreamingResponse
16
  from sqlalchemy.ext.asyncio import AsyncSession
17
 
18
  from app.dependencies import get_db, get_current_user, get_redis
19
- from app.services.file_validator import validate_file
20
- from app.services.parser import parse_document
21
- from app.services.chunker import chunk_text
22
- from app.services.embeddings import embed_and_store
23
- from app.services.summarizer import generate_summary
24
  from app.repositories.book_repo import BookRepository
25
  from app.repositories.document_repo import DocumentRepository
26
  from app.config import get_settings
@@ -41,42 +39,42 @@ async def upload_book(
41
  db: AsyncSession = Depends(get_db),
42
  redis=Depends(get_redis),
43
  ):
44
- """Upload a book file (PDF/EPUB/DOCX/TXT) and start async ingestion.
45
-
46
- Responds immediately with book_id. Use /progress SSE to track ingestion.
47
- """
48
- # Validate file (magic bytes, not just extension)
49
  contents = await file.read()
50
- ext = validate_file(contents, file.filename or "")
51
 
52
  book_repo = BookRepository(db)
53
  doc_repo = DocumentRepository(db)
54
 
55
  book = await book_repo.create(
56
  author_id=current_user.id,
57
- title=title or (file.filename or "Untitled").rsplit(".", 1)[0],
58
  genre=genre,
59
  price=price,
60
  buy_url=buy_url,
61
  status="processing",
62
  )
63
 
 
 
 
 
 
 
 
64
  doc = await doc_repo.create(
65
  book_id=book.id,
66
  author_id=current_user.id,
67
- filename=file.filename or "upload",
68
- file_extension=ext,
69
  file_size_bytes=len(contents),
70
  )
71
  await db.commit()
72
 
73
- # Fire async ingestion in background
74
  asyncio.create_task(
75
  _run_ingestion(
76
- contents=contents,
77
- ext=ext,
78
  book_id=book.id,
79
- doc_id=doc.id,
80
  author_id=current_user.id,
81
  title=book.title,
82
  redis=redis,
@@ -94,7 +92,7 @@ async def ingestion_progress(
94
  current_user=Depends(get_current_user),
95
  redis=Depends(get_redis),
96
  ):
97
- """SSE stream for real-time ingestion progress of a specific book."""
98
  async def event_stream():
99
  channel = f"ingestion:{current_user.id}:{book_id}"
100
  pubsub = redis.pubsub()
@@ -117,36 +115,46 @@ async def ingestion_progress(
117
 
118
 
119
  async def _publish(redis, channel: str, stage: str, progress: int, detail: str = "") -> None:
120
- """Publish an SSE progress event to Redis pub/sub."""
121
- await redis.publish(channel, json.dumps({"stage": stage, "progress": progress, "detail": detail}))
 
 
 
122
 
123
 
124
  async def _run_ingestion(
125
- contents: bytes,
126
- ext: str,
127
  book_id: str,
128
- doc_id: str,
129
  author_id: str,
130
  title: str,
131
  redis,
132
  db: AsyncSession,
133
  ) -> None:
134
- """Full async ingestion: parse β†’ chunk β†’ embed β†’ summarize."""
135
  channel = f"ingestion:{author_id}:{book_id}"
136
  book_repo = BookRepository(db)
137
 
138
  try:
139
- await _publish(redis, channel, "parsing", 10)
140
- text = parse_document(contents, ext)
 
 
 
 
 
 
141
 
142
- await _publish(redis, channel, "chunking", 30)
143
- chunks = chunk_text(text)
 
144
 
145
- await _publish(redis, channel, "embedding", 50)
 
146
  collection_name = await embed_and_store(chunks, author_id, book_id, title)
147
 
148
- await _publish(redis, channel, "summarizing", 80)
149
- summary = await generate_summary(text[:3000], title)
 
150
 
151
  await book_repo.update(book_id, {
152
  "status": "active",
@@ -155,10 +163,17 @@ async def _run_ingestion(
155
  "chunk_count": len(chunks),
156
  })
157
  await db.commit()
158
-
159
  await _publish(redis, channel, "complete", 100)
160
 
161
  except Exception as e:
162
  await book_repo.update(book_id, {"status": "failed"})
163
  await db.commit()
164
  await _publish(redis, channel, "error", 0, str(e))
 
 
 
 
 
 
 
 
 
4
  POST /api/admin/{author_slug}/books/upload β€” Upload a book file
5
  GET /api/admin/{author_slug}/books/upload/progress β€” SSE ingestion progress
6
 
7
+ Saves uploaded file to disk first, then passes the path to services
8
+ that expect file_path (not raw bytes).
9
  """
10
 
11
  import asyncio
12
  import json
13
+ import os
14
+ import tempfile
15
+ from pathlib import Path
16
 
17
  from fastapi import APIRouter, Depends, File, UploadFile, HTTPException
18
  from fastapi.responses import StreamingResponse
19
  from sqlalchemy.ext.asyncio import AsyncSession
20
 
21
  from app.dependencies import get_db, get_current_user, get_redis
 
 
 
 
 
22
  from app.repositories.book_repo import BookRepository
23
  from app.repositories.document_repo import DocumentRepository
24
  from app.config import get_settings
 
39
  db: AsyncSession = Depends(get_db),
40
  redis=Depends(get_redis),
41
  ):
42
+ """Upload a book file (PDF/EPUB/DOCX/TXT) and start async ingestion."""
 
 
 
 
43
  contents = await file.read()
44
+ filename = file.filename or "upload"
45
 
46
  book_repo = BookRepository(db)
47
  doc_repo = DocumentRepository(db)
48
 
49
  book = await book_repo.create(
50
  author_id=current_user.id,
51
+ title=title or filename.rsplit(".", 1)[0],
52
  genre=genre,
53
  price=price,
54
  buy_url=buy_url,
55
  status="processing",
56
  )
57
 
58
+ # Save to temp dir so file-path-based services can access it
59
+ upload_dir = Path(cfg.UPLOAD_STORAGE_PATH)
60
+ upload_dir.mkdir(parents=True, exist_ok=True)
61
+ suffix = Path(filename).suffix or ".bin"
62
+ tmp_path = upload_dir / f"{book.id}{suffix}"
63
+ tmp_path.write_bytes(contents)
64
+
65
  doc = await doc_repo.create(
66
  book_id=book.id,
67
  author_id=current_user.id,
68
+ filename=filename,
69
+ file_extension=suffix.lstrip("."),
70
  file_size_bytes=len(contents),
71
  )
72
  await db.commit()
73
 
 
74
  asyncio.create_task(
75
  _run_ingestion(
76
+ file_path=str(tmp_path),
 
77
  book_id=book.id,
 
78
  author_id=current_user.id,
79
  title=book.title,
80
  redis=redis,
 
92
  current_user=Depends(get_current_user),
93
  redis=Depends(get_redis),
94
  ):
95
+ """SSE stream for real-time ingestion progress."""
96
  async def event_stream():
97
  channel = f"ingestion:{current_user.id}:{book_id}"
98
  pubsub = redis.pubsub()
 
115
 
116
 
117
  async def _publish(redis, channel: str, stage: str, progress: int, detail: str = "") -> None:
118
+ if redis:
119
+ try:
120
+ await redis.publish(channel, json.dumps({"stage": stage, "progress": progress, "detail": detail}))
121
+ except Exception:
122
+ pass
123
 
124
 
125
  async def _run_ingestion(
126
+ file_path: str,
 
127
  book_id: str,
 
128
  author_id: str,
129
  title: str,
130
  redis,
131
  db: AsyncSession,
132
  ) -> None:
133
+ """Full async ingestion: validate β†’ parse β†’ chunk β†’ embed β†’ summarize."""
134
  channel = f"ingestion:{author_id}:{book_id}"
135
  book_repo = BookRepository(db)
136
 
137
  try:
138
+ await _publish(redis, channel, "validating", 5)
139
+ from app.services.file_validator import validate_file
140
+ ext = validate_file(file_path)
141
+
142
+ await _publish(redis, channel, "parsing", 20)
143
+ from app.services.parser import parse_document
144
+ parse_result = parse_document(file_path, ext)
145
+ text = parse_result.text if hasattr(parse_result, "text") else str(parse_result)
146
 
147
+ await _publish(redis, channel, "chunking", 40)
148
+ from app.services.chunker import chunk_document
149
+ chunks = chunk_document(text)
150
 
151
+ await _publish(redis, channel, "embedding", 60)
152
+ from app.services.embeddings import embed_and_store
153
  collection_name = await embed_and_store(chunks, author_id, book_id, title)
154
 
155
+ await _publish(redis, channel, "summarizing", 85)
156
+ from app.services.summarizer import summarize_book
157
+ summary = await summarize_book(text[:3000])
158
 
159
  await book_repo.update(book_id, {
160
  "status": "active",
 
163
  "chunk_count": len(chunks),
164
  })
165
  await db.commit()
 
166
  await _publish(redis, channel, "complete", 100)
167
 
168
  except Exception as e:
169
  await book_repo.update(book_id, {"status": "failed"})
170
  await db.commit()
171
  await _publish(redis, channel, "error", 0, str(e))
172
+
173
+ finally:
174
+ # Clean up temp file
175
+ try:
176
+ if os.path.exists(file_path):
177
+ os.remove(file_path)
178
+ except Exception:
179
+ pass