Skip to content

Commit 48c5aab

Browse files
committed
Flatten source modules to root with layered naming
1 parent a8f5ec7 commit 48c5aab

14 files changed

Lines changed: 123 additions & 82 deletions

PROJECT_STRUCTURE.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
# Project Structure
22

3-
Only the files listed below are retained. Any file not listed here must be removed.
3+
All source files are in the workspace root. Layering is preserved by filename prefix.
44

55
/.gitignore
66
/.data/
77
/README.md
88
/PROJECT_STRUCTURE.md
99
/pyproject.toml
10-
/vra/__init__.py
11-
/vra/__main__.py
12-
/vra/config.py
13-
/vra/media.py
14-
/vra/transcribe.py
15-
/vra/summarize.py
16-
/vra/storage.py
17-
/vra/rss.py
18-
/vra/pipeline.py
19-
/vra/api.py
10+
/vra.py
11+
/cli.py
12+
/core_config.py
13+
/service_media.py
14+
/service_transcribe.py
15+
/service_summarize.py
16+
/service_pipeline.py
17+
/adapter_api.py
18+
/adapter_rss.py
19+
/adapter_storage.py

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ python -m vra serve --bind 0.0.0.0:8080
3434

3535
Models are downloaded from Hugging Face automatically on first run.
3636

37+
## Project Layout
38+
39+
The codebase is organized by layer and naming prefix in the project root:
40+
41+
- `core_*.py`: core runtime config (`core_config.py`)
42+
- `service_*.py`: media preparation, transcription, summarization, and orchestration
43+
- `adapter_*.py`: FastAPI interface, RSS rendering, and database adapter
44+
- `cli.py`: CLI commands (`serve`, `verify`)
45+
- `vra.py`: module entry for `python -m vra`
46+
3747
## Environment Variables
3848

3949
| Variable | Default | Description |

vra/api.py renamed to adapter_api.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from dataclasses import asdict
55
from datetime import datetime, timezone
66

7-
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
8-
from fastapi.responses import PlainTextResponse, Response
7+
from fastapi import Depends, FastAPI, Header, HTTPException, Query
8+
from fastapi.responses import Response
99
from pydantic import BaseModel
1010

11-
from .pipeline import Pipeline
11+
from service_pipeline import Pipeline
1212

1313

1414
class IngestRequest(BaseModel):
@@ -29,7 +29,9 @@ def create_app(pipeline: Pipeline, api_key: str | None = None) -> FastAPI:
2929
rss_link = os.environ.get("VRA_RSS_LINK", "http://localhost:8080/rss")
3030
rss_desc = os.environ.get("VRA_RSS_DESCRIPTION", "Video summaries")
3131

32-
def _check_auth(authorization: str | None = Header(None), x_api_key: str | None = Header(None)):
32+
def _check_auth(
33+
authorization: str | None = Header(None), x_api_key: str | None = Header(None)
34+
):
3335
if api_key is None:
3436
return
3537
token = None
@@ -57,7 +59,9 @@ async def process(req: ProcessRequest, _=Depends(_check_auth)):
5759
return {
5860
"source_url": report.source_url,
5961
"title": report.title,
60-
"transcription": asdict(report.transcription) if report.transcription else None,
62+
"transcription": asdict(report.transcription)
63+
if report.transcription
64+
else None,
6165
"summary": asdict(report.summary) if report.summary else None,
6266
}
6367

vra/rss.py renamed to adapter_rss.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime, timezone
44
from xml.etree.ElementTree import Element, SubElement, tostring
55

6-
from .storage import SummaryRecord
6+
from adapter_storage import SummaryRecord
77

88

99
def render_feed(
@@ -32,7 +32,9 @@ def render_feed(
3232
if rec.published_at:
3333
SubElement(item, "pubDate").text = _rfc2822(rec.published_at)
3434

35-
return '<?xml version="1.0" encoding="UTF-8"?>\n' + tostring(rss, encoding="unicode")
35+
return '<?xml version="1.0" encoding="UTF-8"?>\n' + tostring(
36+
rss, encoding="unicode"
37+
)
3638

3739

3840
def _rfc2822(dt: datetime) -> str:

vra/storage.py renamed to adapter_storage.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import asyncpg
1010

11-
from .transcribe import TranscriptionResult
12-
from .summarize import SummaryResult
11+
from service_summarize import SummaryResult
12+
from service_transcribe import TranscriptionResult
1313

1414
log = logging.getLogger(__name__)
1515

@@ -78,7 +78,9 @@ async def upsert_feed(self, url: str, title: str | None) -> uuid.UUID:
7878
DO UPDATE SET title = COALESCE(EXCLUDED.title, feeds.title),
7979
last_checked = NOW()
8080
RETURNING id""",
81-
fid, url, title,
81+
fid,
82+
url,
83+
title,
8284
)
8385
return row["id"]
8486

@@ -102,17 +104,27 @@ async def upsert_video(
102104
guid = COALESCE(EXCLUDED.guid, videos.guid),
103105
feed_id = COALESCE(EXCLUDED.feed_id, videos.feed_id)
104106
RETURNING id""",
105-
vid, feed_id, source_url, guid, title, published_at,
107+
vid,
108+
feed_id,
109+
source_url,
110+
guid,
111+
title,
112+
published_at,
106113
)
107114
return row["id"]
108115

109-
async def insert_transcript(self, video_id: uuid.UUID, tr: TranscriptionResult) -> uuid.UUID:
116+
async def insert_transcript(
117+
self, video_id: uuid.UUID, tr: TranscriptionResult
118+
) -> uuid.UUID:
110119
tid = uuid.uuid4()
111120
async with self._pool.acquire() as conn:
112121
await conn.execute(
113122
"""INSERT INTO transcripts (id, video_id, language, text)
114123
VALUES ($1, $2, $3, $4)""",
115-
tid, video_id, tr.language, tr.text,
124+
tid,
125+
video_id,
126+
tr.language,
127+
tr.text,
116128
)
117129
return tid
118130

@@ -122,7 +134,10 @@ async def insert_summary(self, video_id: uuid.UUID, sr: SummaryResult) -> uuid.U
122134
await conn.execute(
123135
"""INSERT INTO summaries (id, video_id, summary, key_points)
124136
VALUES ($1, $2, $3, $4::jsonb)""",
125-
sid, video_id, sr.summary, json.dumps(sr.key_points),
137+
sid,
138+
video_id,
139+
sr.summary,
140+
json.dumps(sr.key_points),
126141
)
127142
return sid
128143

@@ -142,12 +157,14 @@ async def latest_summaries(self, limit: int = 20) -> list[SummaryRecord]:
142157
kp = r["key_points"]
143158
if isinstance(kp, str):
144159
kp = json.loads(kp)
145-
out.append(SummaryRecord(
146-
title=r["title"],
147-
source_url=r["source_url"],
148-
published_at=r["published_at"],
149-
summary=r["summary"],
150-
key_points=kp if isinstance(kp, list) else [],
151-
created_at=r["created_at"],
152-
))
160+
out.append(
161+
SummaryRecord(
162+
title=r["title"],
163+
source_url=r["source_url"],
164+
published_at=r["published_at"],
165+
summary=r["summary"],
166+
key_points=kp if isinstance(kp, list) else [],
167+
created_at=r["created_at"],
168+
)
169+
)
153170
return out

vra/__main__.py renamed to cli.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
import click
88
import uvicorn
99

10-
from .config import Config
10+
from core_config import Config
1111

1212
log = logging.getLogger(__name__)
1313

1414

1515
@click.group()
1616
def cli():
17-
"""Video RSS Aggregator Qwen3 intelligent video summarization."""
17+
"""Video RSS Aggregator - Qwen3 intelligent video summarization."""
1818
logging.basicConfig(
1919
level=logging.INFO,
2020
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
@@ -34,8 +34,8 @@ def serve(bind: str | None):
3434
host, port = config.bind_host, config.bind_port
3535

3636
async def _run():
37-
from .api import create_app
38-
from .pipeline import Pipeline
37+
from adapter_api import create_app
38+
from service_pipeline import Pipeline
3939

4040
pipeline = await Pipeline.create(config)
4141
app = create_app(pipeline, config.api_key)
@@ -48,7 +48,9 @@ async def _run():
4848

4949
@cli.command()
5050
@click.option("--feed-url", envvar="VRA_VERIFY_FEED_URL", required=True)
51-
@click.option("--source", envvar="VRA_VERIFY_SOURCE", required=True, help="Audio path or URL")
51+
@click.option(
52+
"--source", envvar="VRA_VERIFY_SOURCE", required=True, help="Audio path or URL"
53+
)
5254
def verify(feed_url: str, source: str):
5355
"""Run end-to-end verification with real data."""
5456
import json
@@ -57,7 +59,7 @@ def verify(feed_url: str, source: str):
5759
config = Config.from_env()
5860

5961
async def _run():
60-
from .pipeline import Pipeline
62+
from service_pipeline import Pipeline
6163

6264
pipeline = await Pipeline.create(config)
6365

@@ -76,7 +78,7 @@ async def _run():
7678
process_ms = int((time.monotonic() - t2) * 1000)
7779

7880
t3 = time.monotonic()
79-
rss = await pipeline.rss_feed("Verification", feed_url, "test", 10)
81+
await pipeline.rss_feed("Verification", feed_url, "test", 10)
8082
rss_ms = int((time.monotonic() - t3) * 1000)
8183

8284
total_ms = int((time.monotonic() - t0) * 1000)
@@ -85,8 +87,12 @@ async def _run():
8587
"feed_url": feed_url,
8688
"feed_items": feed_report.item_count,
8789
"processed_source": source,
88-
"transcription_chars": len(proc_report.transcription.text) if proc_report.transcription else 0,
89-
"summary_chars": len(proc_report.summary.summary) if proc_report.summary else 0,
90+
"transcription_chars": len(proc_report.transcription.text)
91+
if proc_report.transcription
92+
else 0,
93+
"summary_chars": len(proc_report.summary.summary)
94+
if proc_report.summary
95+
else 0,
9096
"total_ms": total_ms,
9197
"feed_ms": feed_ms,
9298
"process_ms": process_ms,
@@ -95,7 +101,3 @@ async def _run():
95101
print(json.dumps(report, indent=2))
96102

97103
asyncio.run(_run())
98-
99-
100-
if __name__ == "__main__":
101-
cli()

vra/config.py renamed to core_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import os
4-
from dataclasses import dataclass, field
4+
from dataclasses import dataclass
55

66

77
@dataclass(frozen=True, slots=True)

pyproject.toml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,22 @@ dependencies = [
1818
]
1919

2020
[project.scripts]
21-
vra = "vra.__main__:cli"
21+
vra = "cli:cli"
2222

2323
[build-system]
2424
requires = ["setuptools>=75"]
2525
build-backend = "setuptools.build_meta"
2626

27-
[tool.setuptools.packages.find]
28-
include = ["vra*"]
27+
[tool.setuptools]
28+
py-modules = [
29+
"vra",
30+
"cli",
31+
"core_config",
32+
"service_media",
33+
"service_transcribe",
34+
"service_summarize",
35+
"service_pipeline",
36+
"adapter_api",
37+
"adapter_rss",
38+
"adapter_storage",
39+
]
File renamed without changes.

vra/pipeline.py renamed to service_pipeline.py

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
from __future__ import annotations
22

33
import asyncio
4-
import logging
5-
from dataclasses import dataclass, field
6-
from io import BytesIO
4+
from dataclasses import dataclass
75

86
import feedparser
97
import httpx
108

11-
from .config import Config
12-
from .media import prepare_audio
13-
from .rss import render_feed
14-
from .storage import Database
15-
from .summarize import SummarizationEngine, SummaryResult
16-
from .transcribe import TranscriptionEngine, TranscriptionResult
17-
18-
log = logging.getLogger(__name__)
9+
from adapter_rss import render_feed
10+
from adapter_storage import Database
11+
from core_config import Config
12+
from service_media import prepare_audio
13+
from service_summarize import SummarizationEngine, SummaryResult
14+
from service_transcribe import TranscriptionEngine, TranscriptionResult
1915

2016

2117
@dataclass(slots=True)
@@ -52,16 +48,11 @@ async def create(cls, config: Config) -> Pipeline:
5248
db = await Database.connect(config.database_url)
5349
await db.migrate()
5450

55-
# Load models in background threads to keep event loop responsive
5651
transcriber = await asyncio.to_thread(TranscriptionEngine.get, config)
5752
summarizer = await asyncio.to_thread(SummarizationEngine.get, config)
5853

5954
return cls(config, db, transcriber, summarizer)
6055

61-
# ------------------------------------------------------------------
62-
# Public API
63-
# ------------------------------------------------------------------
64-
6556
async def ingest_feed(
6657
self,
6758
feed_url: str,
@@ -115,10 +106,6 @@ async def rss_feed(
115106
records = await self._db.latest_summaries(limit)
116107
return render_feed(title, link, description, records)
117108

118-
# ------------------------------------------------------------------
119-
# Internal
120-
# ------------------------------------------------------------------
121-
122109
async def _process_with_video(
123110
self,
124111
video_id,
@@ -146,11 +133,6 @@ async def _process_with_video(
146133
)
147134

148135

149-
# ------------------------------------------------------------------
150-
# Helpers
151-
# ------------------------------------------------------------------
152-
153-
154136
def _pick_source_url(entry) -> str | None:
155137
enclosures = entry.get("enclosures", [])
156138
if enclosures:

0 commit comments

Comments
 (0)