SurfSense/surfsense_backend/app/capabilities/tiktok/comments/schemas.py
CREDO23 7723b5b8b6 feat(tiktok): add tiktok.comments verb for public comment scraping
Comments load over a signed /api/comment/list XHR that TikTok serves to
anonymous sessions once the comments panel is opened (unlike profile-video and
general-search feeds), so this is a reliable verb. Given video URLs it returns
CommentItems (text, author, likes, reply counts; replies carry repliesToId),
deduped per video, capped, and degraded to an ErrorItem for empty/withheld
videos or a bad_url ErrorItem for non-video inputs.

Generalizes the browser capture over a pluggable interaction step so the
comments flow (open panel, scroll the panel to paginate) reuses the same
warm+capture scaffolding as listing/user-search. Billed per comment on a new
TIKTOK_COMMENT meter (TIKTOK_MICROS_PER_COMMENT, matching the per-comment
market), surfaced on the chat subagent alongside tiktok.scrape/user_search.
2026-07-09 18:35:26 +02:00

56 lines
1.8 KiB
Python

"""``tiktok.comments`` I/O contracts.
URL-only: given TikTok video URLs, return each video's public comment thread.
Unlike profile-video/general-search feeds, ``/api/comment/list`` is served to
anonymous sessions once the comments panel opens, so this verb is reliable. Each
result is a :class:`CommentItem` (top-level comments; replies carry ``repliesToId``).
"""
from __future__ import annotations
from pydantic import BaseModel, Field
from app.capabilities.tiktok.scrape.schemas import (
MAX_TIKTOK_ITEMS,
MAX_TIKTOK_SOURCES,
)
from app.proprietary.platforms.tiktok import CommentItem
class CommentsInput(BaseModel):
video_urls: list[str] = Field(
min_length=1,
max_length=MAX_TIKTOK_SOURCES,
description="TikTok video URLs (/@<user>/video/<id>) to pull comments from.",
)
comments_per_video: int = Field(
default=20,
ge=1,
le=MAX_TIKTOK_ITEMS,
description="Max comments to return per video.",
)
max_items: int = Field(
default=20,
ge=1,
le=MAX_TIKTOK_ITEMS,
description="Max total comments to return across all videos.",
)
@property
def estimated_units(self) -> int:
"""Worst-case billable comments for the pre-flight gate: ``max_items`` is a
hard cross-video ceiling (le=100), so no call can exceed it."""
return self.max_items
class CommentsOutput(BaseModel):
items: list[CommentItem] = Field(
default_factory=list,
description="One item per comment returned, in emission order.",
)
@property
def billable_units(self) -> int:
"""One returned comment = one billable unit; ErrorItems (``errorCode`` set,
for bad URLs or empty/withheld videos) are surfaced but never charged."""
return sum(1 for item in self.items if not getattr(item, "errorCode", None))