plan report 이름 통일 및 코드 정리
parent
9b4e99abf9
commit
42e09ae2d1
|
|
@ -1,6 +1,6 @@
|
||||||
from .clinics import router as clinics_router
|
from .clinics import router as clinics_router
|
||||||
from .analysis import router as analysis_router
|
from .analysis import router as analysis_router
|
||||||
from .reports import router as reports_router
|
from .report import router as report_router
|
||||||
from .plans import router as plans_router
|
from .plan import router as plan_router
|
||||||
|
|
||||||
routers = [clinics_router, analysis_router, reports_router, plans_router]
|
routers = [clinics_router, analysis_router, report_router, plan_router]
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ from common.deps import verify_api_key
|
||||||
from integrations.llm.schemas.plan import PlanOutput
|
from integrations.llm.schemas.plan import PlanOutput
|
||||||
from models.plan import PlanApiResponse
|
from models.plan import PlanApiResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/plans", tags=["plans"], dependencies=[Depends(verify_api_key)])
|
router = APIRouter(prefix="/api/plan", tags=["plan"], dependencies=[Depends(verify_api_key)])
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{run_id}", response_model=PlanApiResponse)
|
@router.get("/{run_id}", response_model=PlanApiResponse, response_model_by_alias=True)
|
||||||
async def get_plan(run_id: str):
|
async def get_plan(run_id: str):
|
||||||
logger.info("GET /api/plans/%s", run_id)
|
logger.info("GET /api/plan/%s", run_id)
|
||||||
row = await fetchone(
|
row = await fetchone(
|
||||||
"SELECT ar.plan_data, ar.created_at, h.hospital_name, h.hospital_name_en, h.url"
|
"SELECT ar.plan_data, ar.created_at, h.hospital_name, h.hospital_name_en, h.url"
|
||||||
" FROM analysis_runs ar"
|
" FROM analysis_runs ar"
|
||||||
|
|
@ -28,9 +28,9 @@ async def get_plan(run_id: str):
|
||||||
plan = PlanOutput(**data)
|
plan = PlanOutput(**data)
|
||||||
return PlanApiResponse(
|
return PlanApiResponse(
|
||||||
id=run_id,
|
id=run_id,
|
||||||
clinicName=row["hospital_name"],
|
clinic_name=row["hospital_name"],
|
||||||
clinicNameEn=row["hospital_name_en"],
|
clinic_name_en=row["hospital_name_en"],
|
||||||
createdAt=str(row["created_at"]),
|
created_at=str(row["created_at"]),
|
||||||
targetUrl=row["url"],
|
target_url=row["url"],
|
||||||
**plan.model_dump(),
|
**plan.model_dump(),
|
||||||
)
|
)
|
||||||
|
|
@ -6,13 +6,13 @@ from common.deps import verify_api_key
|
||||||
from integrations.llm.schemas.report import ReportOutput
|
from integrations.llm.schemas.report import ReportOutput
|
||||||
from models.report import MarketingReportResponse
|
from models.report import MarketingReportResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/reports", tags=["reports"], dependencies=[Depends(verify_api_key)])
|
router = APIRouter(prefix="/api/report", tags=["report"], dependencies=[Depends(verify_api_key)])
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{run_id}", response_model=MarketingReportResponse, response_model_by_alias=True)
|
@router.get("/{run_id}", response_model=MarketingReportResponse, response_model_by_alias=True)
|
||||||
async def get_report(run_id: str):
|
async def get_report(run_id: str):
|
||||||
logger.info("GET /api/reports/%s", run_id)
|
logger.info("GET /api/report/%s", run_id)
|
||||||
row = await fetchone(
|
row = await fetchone(
|
||||||
"SELECT ar.report_data, ar.created_at, h.hospital_name, h.hospital_name_en, h.url"
|
"SELECT ar.report_data, ar.created_at, h.hospital_name, h.hospital_name_en, h.url"
|
||||||
" FROM analysis_runs ar"
|
" FROM analysis_runs ar"
|
||||||
|
|
@ -26,10 +26,11 @@ async def get_report(run_id: str):
|
||||||
return Response(status_code=204)
|
return Response(status_code=204)
|
||||||
data = json.loads(row["report_data"]) if isinstance(row["report_data"], str) else row["report_data"]
|
data = json.loads(row["report_data"]) if isinstance(row["report_data"], str) else row["report_data"]
|
||||||
llm_output = ReportOutput(**data)
|
llm_output = ReportOutput(**data)
|
||||||
response = MarketingReportResponse.model_validate(llm_output.model_dump())
|
return MarketingReportResponse(
|
||||||
response.id = run_id
|
id=run_id,
|
||||||
response.clinic_name = row["hospital_name"]
|
clinic_name=row["hospital_name"],
|
||||||
response.clinic_name_en = row["hospital_name_en"]
|
clinic_name_en=row["hospital_name_en"],
|
||||||
response.created_at = str(row["created_at"])
|
created_at=str(row["created_at"]),
|
||||||
response.target_url = row["url"]
|
target_url=row["url"],
|
||||||
return response
|
**llm_output.model_dump(exclude={"id", "created_at", "target_url"}),
|
||||||
|
)
|
||||||
|
|
@ -25,7 +25,7 @@ class FontSpec(BaseModel):
|
||||||
family: str
|
family: str
|
||||||
weight: str
|
weight: str
|
||||||
usage: str
|
usage: str
|
||||||
sampleText: str
|
sample_text: str
|
||||||
|
|
||||||
|
|
||||||
class LogoUsageRule(BaseModel):
|
class LogoUsageRule(BaseModel):
|
||||||
|
|
@ -36,29 +36,29 @@ class LogoUsageRule(BaseModel):
|
||||||
|
|
||||||
class ToneOfVoice(BaseModel):
|
class ToneOfVoice(BaseModel):
|
||||||
personality: list[str]
|
personality: list[str]
|
||||||
communicationStyle: str
|
communication_style: str
|
||||||
doExamples: list[str]
|
do_examples: list[str]
|
||||||
dontExamples: list[str]
|
dont_examples: list[str]
|
||||||
|
|
||||||
|
|
||||||
class ChannelBrandingRule(BaseModel):
|
class ChannelBrandingRule(BaseModel):
|
||||||
channel: str
|
channel: str
|
||||||
icon: str
|
icon: str
|
||||||
profilePhoto: str
|
profile_photo: str
|
||||||
bannerSpec: str
|
banner_spec: str
|
||||||
bioTemplate: str
|
bio_template: str
|
||||||
currentStatus: Literal["correct", "incorrect", "missing"]
|
current_status: Literal["correct", "incorrect", "missing"]
|
||||||
|
|
||||||
|
|
||||||
class BrandInconsistencyValue(BaseModel):
|
class BrandPlanInconsistencyValue(BaseModel):
|
||||||
channel: str
|
channel: str
|
||||||
value: str
|
value: str
|
||||||
isCorrect: bool
|
is_correct: bool
|
||||||
|
|
||||||
|
|
||||||
class BrandInconsistency(BaseModel):
|
class BrandPlanInconsistency(BaseModel):
|
||||||
field: str
|
field: str
|
||||||
values: list[BrandInconsistencyValue]
|
values: list[BrandPlanInconsistencyValue]
|
||||||
impact: str
|
impact: str
|
||||||
recommendation: str
|
recommendation: str
|
||||||
|
|
||||||
|
|
@ -66,26 +66,26 @@ class BrandInconsistency(BaseModel):
|
||||||
class BrandGuide(BaseModel):
|
class BrandGuide(BaseModel):
|
||||||
colors: list[ColorSwatch]
|
colors: list[ColorSwatch]
|
||||||
fonts: list[FontSpec]
|
fonts: list[FontSpec]
|
||||||
logoRules: list[LogoUsageRule]
|
logo_rules: list[LogoUsageRule]
|
||||||
toneOfVoice: ToneOfVoice
|
tone_of_voice: ToneOfVoice
|
||||||
channelBranding: list[ChannelBrandingRule]
|
channel_branding: list[ChannelBrandingRule]
|
||||||
brandInconsistencies: list[BrandInconsistency]
|
brand_inconsistencies: list[BrandPlanInconsistency]
|
||||||
|
|
||||||
|
|
||||||
# --- ChannelStrategy ---
|
# --- ChannelStrategy ---
|
||||||
|
|
||||||
class ChannelStrategyCard(BaseModel):
|
class ChannelStrategyCard(BaseModel):
|
||||||
channelId: str
|
channel_id: str
|
||||||
channelName: str
|
channel_name: str
|
||||||
icon: str
|
icon: str
|
||||||
currentStatus: str
|
current_status: str
|
||||||
targetGoal: str
|
target_goal: str
|
||||||
contentTypes: list[str]
|
content_types: list[str]
|
||||||
postingFrequency: str
|
posting_frequency: str
|
||||||
tone: str
|
tone: str
|
||||||
formatGuidelines: list[str]
|
format_guidelines: list[str]
|
||||||
priority: Literal["P0", "P1", "P2"]
|
priority: Literal["P0", "P1", "P2"]
|
||||||
customerJourneyStage: Literal["awareness", "interest", "consideration", "conversion", "loyalty"] | None = None
|
customer_journey_stage: Literal["awareness", "interest", "consideration", "conversion", "loyalty"] | None = None
|
||||||
|
|
||||||
|
|
||||||
# --- ContentStrategy ---
|
# --- ContentStrategy ---
|
||||||
|
|
@ -93,8 +93,8 @@ class ChannelStrategyCard(BaseModel):
|
||||||
class ContentPillar(BaseModel):
|
class ContentPillar(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
relatedUSP: str
|
related_usp: str
|
||||||
exampleTopics: list[str]
|
example_topics: list[str]
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -121,30 +121,30 @@ class RepurposingOutput(BaseModel):
|
||||||
|
|
||||||
class ContentStrategyData(BaseModel):
|
class ContentStrategyData(BaseModel):
|
||||||
pillars: list[ContentPillar]
|
pillars: list[ContentPillar]
|
||||||
typeMatrix: list[ContentTypeRow]
|
type_matrix: list[ContentTypeRow]
|
||||||
workflow: list[WorkflowStep]
|
workflow: list[WorkflowStep]
|
||||||
repurposingSource: str
|
repurposing_source: str
|
||||||
repurposingOutputs: list[RepurposingOutput]
|
repurposing_outputs: list[RepurposingOutput]
|
||||||
|
|
||||||
|
|
||||||
# --- Calendar ---
|
# --- Calendar ---
|
||||||
|
|
||||||
class CalendarEntry(BaseModel):
|
class CalendarEntry(BaseModel):
|
||||||
dayOfWeek: int
|
day_of_week: int
|
||||||
channel: str
|
channel: str
|
||||||
channelIcon: str
|
channel_icon: str
|
||||||
contentType: Literal["video", "blog", "social", "ad"]
|
content_type: Literal["video", "blog", "social", "ad"]
|
||||||
title: str
|
title: str
|
||||||
id: str | None = None
|
id: str | None = None
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
pillar: str | None = None
|
pillar: str | None = None
|
||||||
status: Literal["draft", "approved", "published"] | None = None
|
status: Literal["draft", "approved", "published"] | None = None
|
||||||
isManualEdit: bool | None = None
|
is_manual_edit: bool | None = None
|
||||||
aiPromptSeed: str | None = None
|
ai_prompt_seed: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class CalendarWeek(BaseModel):
|
class CalendarWeek(BaseModel):
|
||||||
weekNumber: int
|
week_number: int
|
||||||
label: str
|
label: str
|
||||||
entries: list[CalendarEntry]
|
entries: list[CalendarEntry]
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ class ContentCountSummary(BaseModel):
|
||||||
|
|
||||||
class CalendarData(BaseModel):
|
class CalendarData(BaseModel):
|
||||||
weeks: list[CalendarWeek]
|
weeks: list[CalendarWeek]
|
||||||
monthlySummary: list[ContentCountSummary]
|
monthly_summary: list[ContentCountSummary]
|
||||||
|
|
||||||
|
|
||||||
# --- AssetCollection ---
|
# --- AssetCollection ---
|
||||||
|
|
@ -166,11 +166,11 @@ class CalendarData(BaseModel):
|
||||||
class AssetCard(BaseModel):
|
class AssetCard(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
source: Literal["homepage", "naver_place", "blog", "social", "youtube"]
|
source: Literal["homepage", "naver_place", "blog", "social", "youtube"]
|
||||||
sourceLabel: str
|
source_label: str
|
||||||
type: Literal["photo", "video", "text"]
|
type: Literal["photo", "video", "text"]
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
repurposingSuggestions: list[str]
|
repurposing_suggestions: list[str]
|
||||||
status: Literal["collected", "pending", "needs_creation"]
|
status: Literal["collected", "pending", "needs_creation"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -178,62 +178,29 @@ class YouTubeRepurposeItem(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
views: int
|
views: int
|
||||||
type: Literal["Short", "Long"]
|
type: Literal["Short", "Long"]
|
||||||
repurposeAs: list[str]
|
repurpose_as: list[str]
|
||||||
|
|
||||||
|
|
||||||
class AssetCollectionData(BaseModel):
|
class AssetCollectionData(BaseModel):
|
||||||
assets: list[AssetCard]
|
assets: list[AssetCard]
|
||||||
youtubeRepurpose: list[YouTubeRepurposeItem]
|
youtube_repurpose: list[YouTubeRepurposeItem]
|
||||||
|
|
||||||
|
|
||||||
# --- Repurposing ---
|
# --- Repurposing ---
|
||||||
|
|
||||||
class RepurposingProposalItem(BaseModel):
|
class RepurposingProposalItem(BaseModel):
|
||||||
sourceVideo: YouTubeRepurposeItem
|
source_video: YouTubeRepurposeItem
|
||||||
outputs: list[RepurposingOutput]
|
outputs: list[RepurposingOutput]
|
||||||
estimatedEffort: Literal["low", "medium", "high"]
|
estimated_effort: Literal["low", "medium", "high"]
|
||||||
priority: Literal["high", "medium", "low"]
|
priority: Literal["high", "medium", "low"]
|
||||||
|
|
||||||
|
|
||||||
# --- Workflow ---
|
|
||||||
|
|
||||||
class WorkflowVideoDraft(BaseModel):
|
|
||||||
script: str
|
|
||||||
shootingGuide: list[str]
|
|
||||||
duration: str
|
|
||||||
|
|
||||||
|
|
||||||
# class WorkflowImageTextDraft(BaseModel):
|
|
||||||
# type: Literal["cardnews", "blog"]
|
|
||||||
# headline: str
|
|
||||||
# copy: list[str]
|
|
||||||
# layoutHint: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# class WorkflowItem(BaseModel):
|
|
||||||
# id: str
|
|
||||||
# title: str
|
|
||||||
# contentType: Literal["video", "image-text"]
|
|
||||||
# channel: str
|
|
||||||
# channelIcon: str
|
|
||||||
# stage: Literal["planning", "ai-draft", "review", "approved", "scheduled"]
|
|
||||||
# userNotes: str | None = None
|
|
||||||
# videoDraft: WorkflowVideoDraft | None = None
|
|
||||||
# imageTextDraft: WorkflowImageTextDraft | None = None
|
|
||||||
# scheduledDate: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# class WorkflowData(BaseModel):
|
|
||||||
# items: list[WorkflowItem]
|
|
||||||
|
|
||||||
|
|
||||||
# --- PlanOutput ---
|
# --- PlanOutput ---
|
||||||
|
|
||||||
class PlanOutput(BaseModel):
|
class PlanOutput(BaseModel):
|
||||||
brandGuide: BrandGuide
|
brand_guide: BrandGuide
|
||||||
channelStrategies: list[ChannelStrategyCard]
|
channel_strategies: list[ChannelStrategyCard]
|
||||||
contentStrategy: ContentStrategyData
|
content_strategy: ContentStrategyData
|
||||||
calendar: CalendarData
|
calendar: CalendarData
|
||||||
assetCollection: AssetCollectionData
|
asset_collection: AssetCollectionData
|
||||||
repurposingProposals: list[RepurposingProposalItem] | None = None
|
repurposing_proposals: list[RepurposingProposalItem] | None = None
|
||||||
#workflow: WorkflowData | None = None
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Literal
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from models.status import Severity, ChannelStatus, DataSource, Language, VideoType, AnnotationType
|
||||||
Severity = Literal["critical", "warning", "good", "excellent", "unknown"]
|
|
||||||
ChannelStatus = Literal["active", "inactive", "unknown", "not_found"]
|
|
||||||
DataSource = Literal["registry", "scrape"]
|
|
||||||
Language = Literal["KR", "EN"]
|
|
||||||
VideoType = Literal["Short", "Long"]
|
|
||||||
AnnotationType = Literal["highlight", "arrow", "text"]
|
|
||||||
|
|
||||||
|
|
||||||
class ScreenshotAnnotation(BaseModel):
|
class ScreenshotAnnotation(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
def _to_camel(s: str) -> str:
|
||||||
|
head, *tail = s.split("_")
|
||||||
|
return head + "".join(w.capitalize() for w in tail)
|
||||||
|
|
||||||
|
|
||||||
|
class CamelModel(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True, alias_generator=_to_camel)
|
||||||
|
|
@ -1,19 +1,200 @@
|
||||||
from pydantic import BaseModel
|
from typing import Literal
|
||||||
from integrations.llm.schemas.plan import (
|
from models.common import CamelModel
|
||||||
BrandGuide, ChannelStrategyCard, ContentStrategyData,
|
|
||||||
CalendarData, AssetCollectionData, RepurposingProposalItem,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PlanApiResponse(BaseModel):
|
class BrandPlanInconsistencyValue(CamelModel):
|
||||||
|
channel: str
|
||||||
|
value: str
|
||||||
|
is_correct: bool
|
||||||
|
|
||||||
|
|
||||||
|
class BrandPlanInconsistency(CamelModel):
|
||||||
|
field: str
|
||||||
|
values: list[BrandPlanInconsistencyValue]
|
||||||
|
impact: str
|
||||||
|
recommendation: str
|
||||||
|
|
||||||
|
|
||||||
|
# --- BrandGuide ---
|
||||||
|
|
||||||
|
class ColorSwatch(CamelModel):
|
||||||
|
name: str
|
||||||
|
hex: str
|
||||||
|
usage: str
|
||||||
|
|
||||||
|
|
||||||
|
class FontSpec(CamelModel):
|
||||||
|
family: str
|
||||||
|
weight: str
|
||||||
|
usage: str
|
||||||
|
sample_text: str
|
||||||
|
|
||||||
|
|
||||||
|
class LogoUsageRule(CamelModel):
|
||||||
|
rule: str
|
||||||
|
description: str
|
||||||
|
correct: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ToneOfVoice(CamelModel):
|
||||||
|
personality: list[str]
|
||||||
|
communication_style: str
|
||||||
|
do_examples: list[str]
|
||||||
|
dont_examples: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelBrandingRule(CamelModel):
|
||||||
|
channel: str
|
||||||
|
icon: str
|
||||||
|
profile_photo: str
|
||||||
|
banner_spec: str
|
||||||
|
bio_template: str
|
||||||
|
current_status: Literal["correct", "incorrect", "missing"]
|
||||||
|
|
||||||
|
|
||||||
|
class BrandGuide(CamelModel):
|
||||||
|
colors: list[ColorSwatch]
|
||||||
|
fonts: list[FontSpec]
|
||||||
|
logo_rules: list[LogoUsageRule]
|
||||||
|
tone_of_voice: ToneOfVoice
|
||||||
|
channel_branding: list[ChannelBrandingRule]
|
||||||
|
brand_inconsistencies: list[BrandPlanInconsistency]
|
||||||
|
|
||||||
|
|
||||||
|
# --- ChannelStrategy ---
|
||||||
|
|
||||||
|
class ChannelStrategyCard(CamelModel):
|
||||||
|
channel_id: str
|
||||||
|
channel_name: str
|
||||||
|
icon: str
|
||||||
|
current_status: str
|
||||||
|
target_goal: str
|
||||||
|
content_types: list[str]
|
||||||
|
posting_frequency: str
|
||||||
|
tone: str
|
||||||
|
format_guidelines: list[str]
|
||||||
|
priority: Literal["P0", "P1", "P2"]
|
||||||
|
customer_journey_stage: Literal["awareness", "interest", "consideration", "conversion", "loyalty"] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# --- ContentStrategy ---
|
||||||
|
|
||||||
|
class ContentPillar(CamelModel):
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
related_usp: str
|
||||||
|
example_topics: list[str]
|
||||||
|
color: str
|
||||||
|
|
||||||
|
|
||||||
|
class ContentTypeRow(CamelModel):
|
||||||
|
format: str
|
||||||
|
channels: list[str]
|
||||||
|
frequency: str
|
||||||
|
purpose: str
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowStep(CamelModel):
|
||||||
|
step: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
owner: str
|
||||||
|
duration: str
|
||||||
|
|
||||||
|
|
||||||
|
class RepurposingOutput(CamelModel):
|
||||||
|
format: str
|
||||||
|
channel: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
class ContentStrategyData(CamelModel):
|
||||||
|
pillars: list[ContentPillar]
|
||||||
|
type_matrix: list[ContentTypeRow]
|
||||||
|
workflow: list[WorkflowStep]
|
||||||
|
repurposing_source: str
|
||||||
|
repurposing_outputs: list[RepurposingOutput]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Calendar ---
|
||||||
|
|
||||||
|
class CalendarEntry(CamelModel):
|
||||||
|
day_of_week: int
|
||||||
|
channel: str
|
||||||
|
channel_icon: str
|
||||||
|
content_type: Literal["video", "blog", "social", "ad"]
|
||||||
|
title: str
|
||||||
|
id: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
pillar: str | None = None
|
||||||
|
status: Literal["draft", "approved", "published"] | None = None
|
||||||
|
is_manual_edit: bool | None = None
|
||||||
|
ai_prompt_seed: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarWeek(CamelModel):
|
||||||
|
week_number: int
|
||||||
|
label: str
|
||||||
|
entries: list[CalendarEntry]
|
||||||
|
|
||||||
|
|
||||||
|
class ContentCountSummary(CamelModel):
|
||||||
|
type: Literal["video", "blog", "social", "ad"]
|
||||||
|
label: str
|
||||||
|
count: int
|
||||||
|
color: str
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarData(CamelModel):
|
||||||
|
weeks: list[CalendarWeek]
|
||||||
|
monthly_summary: list[ContentCountSummary]
|
||||||
|
|
||||||
|
|
||||||
|
# --- AssetCollection ---
|
||||||
|
|
||||||
|
class AssetCard(CamelModel):
|
||||||
id: str
|
id: str
|
||||||
clinicName: str | None = None
|
source: Literal["homepage", "naver_place", "blog", "social", "youtube"]
|
||||||
clinicNameEn: str | None = None
|
source_label: str
|
||||||
createdAt: str
|
type: Literal["photo", "video", "text"]
|
||||||
targetUrl: str
|
title: str
|
||||||
brandGuide: BrandGuide
|
description: str
|
||||||
channelStrategies: list[ChannelStrategyCard]
|
repurposing_suggestions: list[str]
|
||||||
contentStrategy: ContentStrategyData
|
status: Literal["collected", "pending", "needs_creation"]
|
||||||
|
|
||||||
|
|
||||||
|
class YouTubeRepurposeItem(CamelModel):
|
||||||
|
title: str
|
||||||
|
views: int
|
||||||
|
type: Literal["Short", "Long"]
|
||||||
|
repurpose_as: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class AssetCollectionData(CamelModel):
|
||||||
|
assets: list[AssetCard]
|
||||||
|
youtube_repurpose: list[YouTubeRepurposeItem]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Repurposing ---
|
||||||
|
|
||||||
|
class RepurposingProposalItem(CamelModel):
|
||||||
|
source_video: YouTubeRepurposeItem
|
||||||
|
outputs: list[RepurposingOutput]
|
||||||
|
estimated_effort: Literal["low", "medium", "high"]
|
||||||
|
priority: Literal["high", "medium", "low"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- PlanApiResponse ---
|
||||||
|
|
||||||
|
class PlanApiResponse(CamelModel):
|
||||||
|
id: str
|
||||||
|
clinic_name: str | None = None
|
||||||
|
clinic_name_en: str | None = None
|
||||||
|
created_at: str
|
||||||
|
target_url: str
|
||||||
|
brand_guide: BrandGuide
|
||||||
|
channel_strategies: list[ChannelStrategyCard]
|
||||||
|
content_strategy: ContentStrategyData
|
||||||
calendar: CalendarData
|
calendar: CalendarData
|
||||||
assetCollection: AssetCollectionData
|
asset_collection: AssetCollectionData
|
||||||
repurposingProposals: list[RepurposingProposalItem] | None = None
|
repurposing_proposals: list[RepurposingProposalItem] | None = None
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pydantic import BaseModel, ConfigDict
|
from models.common import CamelModel
|
||||||
from integrations.llm.schemas.report import (
|
from models.status import Severity, ChannelStatus, DataSource, Language, VideoType, AnnotationType
|
||||||
Severity, ChannelStatus, DataSource, Language, VideoType, AnnotationType,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _to_camel(s: str) -> str:
|
|
||||||
head, *tail = s.split("_")
|
|
||||||
return head + "".join(w.capitalize() for w in tail)
|
|
||||||
|
|
||||||
|
|
||||||
class CamelModel(BaseModel):
|
|
||||||
model_config = ConfigDict(populate_by_name=True, alias_generator=_to_camel)
|
|
||||||
|
|
||||||
|
|
||||||
class ScreenshotAnnotation(CamelModel):
|
class ScreenshotAnnotation(CamelModel):
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,39 @@ class TaskStatus(StrEnum):
|
||||||
START = "start"
|
START = "start"
|
||||||
PROCESSING = "processing"
|
PROCESSING = "processing"
|
||||||
DONE = "done"
|
DONE = "done"
|
||||||
|
|
||||||
|
|
||||||
|
class Severity(StrEnum):
|
||||||
|
CRITICAL = "critical"
|
||||||
|
WARNING = "warning"
|
||||||
|
GOOD = "good"
|
||||||
|
EXCELLENT = "excellent"
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelStatus(StrEnum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
NOT_FOUND = "not_found"
|
||||||
|
|
||||||
|
|
||||||
|
class DataSource(StrEnum):
|
||||||
|
REGISTRY = "registry"
|
||||||
|
SCRAPE = "scrape"
|
||||||
|
|
||||||
|
|
||||||
|
class Language(StrEnum):
|
||||||
|
KR = "KR"
|
||||||
|
EN = "EN"
|
||||||
|
|
||||||
|
|
||||||
|
class VideoType(StrEnum):
|
||||||
|
SHORT = "Short"
|
||||||
|
LONG = "Long"
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationType(StrEnum):
|
||||||
|
HIGHLIGHT = "highlight"
|
||||||
|
ARROW = "arrow"
|
||||||
|
TEXT = "text"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue