[ ... ] ให้ตรงงานคุณก่อน copyสร้างโปรเจกต์ Python FastAPI web app ชื่อ "[ai-content-poster]" โครงสร้างแบบนี้:
โครงสร้างไฟล์:
app.py — FastAPI entry point + lifespan handler
db.py — SQLAlchemy engine + session (SQLite)
models.py — ORM models (ว่างไว้ก่อน มีแค่ Base)
requirements.txt — dependency ทั้งหมด
ui/templates/
base.html — base Jinja2 template + Tailwind CDN + Inter font
home.html — extends base, แสดง "App is running ✓"
Dependencies ที่ต้องมี:
- fastapi, uvicorn[standard], sqlalchemy, jinja2, python-dotenv, httpx, pillow
app.py ต้อง:
- ใช้ @asynccontextmanager lifespan เรียก init_db() ตอน startup
- Mount /static สำหรับ static files
- มี route GET / ที่ render home.html
- อ่าน PORT จาก env (default 8000)
base.html ต้อง:
- มี Tailwind CSS ผ่าน CDN
- ใช้ Inter font จาก Google Fonts
- มี sidebar nav สีเข้ม (ลิงก์ placeholder ไปก่อน)
- content area อยู่ทางขวา
บริบทของฉัน: [ธุรกิจ/แบรนด์ = ..., โทนแอป = ...]
รันด้วย: uvicorn app:app --reload
แสดงไฟล์ทั้งหมดแบบเต็ม
เพิ่ม SQLAlchemy models ใน models.py มี table ดังนี้: 1. Draft — เก็บ content ที่ AI สร้าง ก่อนโพสต์ Columns: id (pk), source_url, title, body (Text), caption (Text), status (String: pending/summarizing/ready/image_gen/posting/posted/failed), pending_post (JSON — wizard state: version_idx, artwork_paths ฯลฯ), created_at, updated_at 2. Post — เก็บโพสต์ที่เผยแพร่สำเร็จ Columns: id (pk), draft_id (FK→Draft), fb_post_id (String, unique, indexed), permalink_url, published_at 3. CostEvent — track ค่า API ต่อ operation Columns: id (pk), model (String), input_tokens, output_tokens, cost_usd (Float), kind (String: summarize/classify/image), created_at กฎ: - timestamp เก็บเป็น UTC ผ่าน datetime.utcnow - Draft.pending_post ใช้ JSON ที่ SQLAlchemy serialize อัตโนมัติ - เพิ่ม index ที่ Draft.status และ Post.fb_post_id - db.py: init_db() ใช้ metadata.create_all() — ไม่ต้องทำ migration แค่ auto-create บริบทของฉัน: [ถ้าอยากเก็บ field เพิ่ม เช่น ..., ให้บอกตรงนี้] แก้ db.py ให้ init_db() log จำนวน draft/post ที่มี ตอน startup แสดง models.py และ db.py แบบเต็ม
สร้าง core/sources.py — ตัวดึงข่าวจาก RSS:
RSS feeds ที่ต้องรองรับ (hardcode เป็น list) — [แก้ให้เป็นแหล่งข่าวที่ฉันอยากใช้]:
- https://feeds.feedburner.com/TechCrunch/
- https://venturebeat.com/feed/
- https://www.theverge.com/rss/index.xml
- https://techcrunch.com/tag/artificial-intelligence/feed/
Functions:
1. fetch_rss_feeds(max_per_feed=10) -> list[dict]
- parse แต่ละ feed ด้วย feedparser
- return list ของ {title, url, summary, published_at, source}
- dedupe ด้วย URL
- กรอง: เอาเฉพาะ 48 ชม.ล่าสุด
2. fetch_article_content(url: str) -> str
- ใช้ httpx + Chrome User-Agent header
- สำคัญ: Accept-Encoding ต้องเป็น "gzip, deflate" (ห้าม "br") — brotli ทำให้ได้ binary garbage
- parse ด้วย BeautifulSoup ดึงเนื้อบทความหลัก
- return 3000 ตัวอักษรแรก
- error ให้ return "" (ห้าม raise)
3. route GET /api/news/browse ใน app.py
- เรียก fetch_rss_feeds()
- return JSON list
- cache 10 นาที (dict cache + timestamp ง่ายๆ)
บริบทของฉัน: [หัวข้อข่าวที่สนใจ = ..., ภาษาแหล่งข่าว = ...]
เพิ่ม feedparser กับ beautifulsoup4 ใน requirements.txt
แสดง core/sources.py แบบเต็ม
สร้าง core/summarizer.py ใช้ Anthropic Python SDK:
1. classify_article(title: str, summary: str) -> str ("USE" หรือ "SKIP")
- ใช้ claude-haiku-4-5 (เร็ว ถูกสุด)
- จัดว่าข่าวเหมาะกับ [กลุ่มเป้าหมายของฉัน = เจ้าของ SME ไทยที่อยากรู้เรื่อง AI] ไหม
- USE: เครื่องมือ AI ใช้จริง, ประยุกต์กับธุรกิจ, AI ในชีวิตประจำวัน
- SKIP: paper วิจัยลึก, เนื้อหาสำหรับ dev เท่านั้น, ข่าวภายในองค์กร
- return "USE" หรือ "SKIP" เท่านั้น
2. summarize_article(title: str, body: str, url: str) -> dict
- ใช้ claude-sonnet-4-6 (คุณภาพดีสุด)
- output เป็น JSON: {
"hook": "hook ภาษาไทย 1 ประโยค ดึงความสนใจสำหรับ Facebook",
"body": "สรุปภาษาไทย 3-4 ย่อหน้า (คุยกันเป็นกันเอง ไม่วิชาการ)",
"caption": "โพสต์ FB เต็ม = hook + body + hashtags + source URL",
"tagline": "ข้อความบนรูป 5-10 คำไทย (ไม่มีจุดท้าย)"
}
- โทน: [อบอุ่น ใช้ได้จริง อ่านง่าย ไม่ทางการ — แก้ให้เป็นโทนแบรนด์ฉัน]
- ใส่ hashtag ไทยที่เกี่ยว 3-5 อัน
- ถ้า body สั้นกว่า 100 ตัวอักษร ให้ใช้ title + summary แทน (fallback ตอน scrape ไม่ได้)
3. track cost: หลังทุก API call ให้ log {model, input_tokens, output_tokens, cost_usd}
(pricing Anthropic: haiku=$0.80/$4 ต่อ 1M, sonnet=$3/$15 ต่อ 1M input/output)
ANTHROPIC_API_KEY อ่านจาก env
แสดง core/summarizer.py แบบเต็ม
สร้าง core/image_maker.py สำหรับสร้างรูปโพสต์ Facebook:
1. generate_ai_image(tagline: str, style_prompt: str) -> str (file path)
- ใช้ openai client, model="gpt-image-2"
- Size: 1024x1024, quality="medium"
- Prompt: f"Modern tech illustration for [Thai social media / แก้เป็นสไตล์ที่ฉันต้องการ]. {style_prompt}. Clean, vibrant."
- บันทึกเป็น PNG ที่ /tmp/artwork_{uuid}.png
- สำคัญ: re-encode เป็น JPEG q=92 ก่อน return (PNG อาจ 3-5MB, FB reject ถ้า >4MB)
- return path ของ JPEG
2. render_color_bg_card(tagline: str, color_hex: str) -> str (file path)
- ใช้ Pillow สร้างรูป 1080x1080
- พื้นหลังสีทึบ color_hex
- วาง tagline ภาษาไทยกลางรูป (ใช้ system font หรือ bundle NotoSansThai)
- ใส่ [ชื่อแบรนด์ฉัน] มุมขวาล่าง ตัวเล็ก
- บันทึกแล้ว return path
3. route serve_image: GET /artwork/{filename}
- serve ไฟล์จาก /tmp/ เป็น image/jpeg หรือ image/png
บริบทของฉัน: [สไตล์ภาพ = ..., สีแบรนด์ = #...]
OPENAI_API_KEY อ่านจาก env
เพิ่ม openai ใน requirements.txt
แสดง core/image_maker.py แบบเต็ม
สร้าง core/facebook.py — wrapper ของ Facebook Graph API:
Environment variables:
FACEBOOK_PAGE_ACCESS_TOKEN — long-lived page token
FACEBOOK_PAGE_ID — page ID (ตัวเลข)
FACEBOOK_DRY_RUN — "1" = simulate เท่านั้น (ไม่เรียก FB API), "0" = โพสต์จริง
Functions:
1. post_photo_to_page(caption: str, image_path: str, scheduled_unix: int | None) -> dict
- ถ้า FACEBOOK_DRY_RUN=1: log แล้ว return ปลอม {"fb_post_id": "DRY_RUN_123", "permalink": ""}
- upload รูปไป FB ผ่าน multipart POST ไป /v22.0/{page_id}/photos
- สำคัญ: re-encode รูปเป็น JPEG bytes ใน memory ก่อน upload (เลี่ยง PNG ใหญ่)
- ถ้ามี scheduled_unix: ใส่ published=false + scheduled_publish_time=unix_ts
- ถ้าไม่มี: published=true
- return {"fb_post_id": str, "permalink_url": str}
- raise ถ้า FB error (parse key "error" จาก response JSON)
2. post_comment(post_id: str, message: str) -> str (comment_id)
- POST ไป /v22.0/{post_id}/comments
- เคารพ flag DRY_RUN
- return comment_id หรือ "DRY_RUN" ถ้า dry
3. list_scheduled_posts() -> list[dict]
- GET /v22.0/{page_id}/scheduled_posts
- return list ของ {id, message, scheduled_publish_time}
หมายเหตุความปลอดภัย: [เริ่มด้วย DRY_RUN=1 เสมอ ทดสอบให้ชัวร์ก่อนค่อยเปิดโพสต์จริง]
Logging: log ทุก API call พร้อม method + endpoint + response status
แสดง core/facebook.py แบบเต็ม
สร้าง core/line_oa.py — โมดูลแจ้งเตือน LINE OA:
Environment variables:
LINE_CHANNEL_ACCESS_TOKEN — channel access token
LINE_GROUP_ID — group/room ID ที่จะ push (optional)
LINE_DRY_RUN — "1" = log อย่างเดียว ไม่เรียก LINE API
Functions:
1. push_post_published(headline: str, caption_preview: str, fb_post_url: str) -> bool
- เรียกหลังโพสต์ FB สำเร็จ
- ส่ง Flex Message ไป LINE_GROUP_ID (หรือ broadcast ถ้าไม่มี group)
- ดีไซน์ Flex Message:
* Header: พื้นเขียว, "✅ โพสต์สำเร็จ!"
* Body: headline (ตัวหนา), preview ของ caption (2 บรรทัด)
* Footer: ปุ่ม "ดูโพสต์ใน Facebook" → เปิด fb_post_url
- return True ถ้าสำเร็จ, False ถ้า error
- ห้าม raise — นี่คือ notification ที่ไม่ critical
2. _build_flex_message(headline, preview, url) -> dict
- return LINE Flex Message JSON ที่ valid
- ใช้ container แบบ "bubble"
3. _call_line_api(endpoint: str, body: dict) -> bool
- POST ไป https://api.line.me/v2/bot/{endpoint}
- Auth: "Bearer {LINE_CHANNEL_ACCESS_TOKEN}"
- log response status
บริบทของฉัน: [ข้อความแจ้งเตือน/ปุ่ม ปรับให้ตรงทีมฉัน]
Integration: ในฟังก์ชันโพสต์ FB (core/facebook.py) หลังโพสต์สำเร็จ
ให้เรียก line_oa.push_post_published() ใน try/except
เพื่อไม่ให้ LINE พังแล้วกระทบผล FB post
แสดง core/line_oa.py แบบเต็ม
สร้าง UI แบบ wizard หลายขั้นตอน ด้วย HTMX 2.0 (CDN) + Tailwind
ไม่ใช้ JS framework — ใช้แค่ HTMX + Jinja2
Route: GET /content → render หน้า wizard หลัก
ขั้นตอน wizard (เก็บ state ใน draft.status + draft.pending_post JSON):
Step 0 — Loading (status=summarizing)
- auto-poll ทุก 3s: hx-get="/content/draft/{id}/step" hx-trigger="every 3s"
- แสดง spinner + "กำลังสรุปข่าว..."
Step 1 — Browse ข่าว
- แสดง list บทความจาก RSS (title, source, วันที่)
- แต่ละแถว: ปุ่ม "เลือกข่าวนี้" → POST /content/drafts (สร้าง draft, เริ่ม job)
- render เป็น HTMX partial, swap เข้า #wizard-frame
Step 2 — เลือกเวอร์ชัน body
- แสดง body ภาษาไทย 3 เวอร์ชัน (radio button)
- ปุ่ม "ถัดไป" → POST /content/draft/{id}/pick-body
Step 3 — สร้างรูป
- แสดงช่อง tagline (เติมมาจาก AI แล้ว)
- ปุ่ม "สร้างรูป" → POST /content/draft/{id}/gen-image (async job)
- poll /content/draft/{id}/step ทุก 1.5s ระหว่าง status=image_gen
Step 4 — เลือกรูป
- แสดงรูป 2 รูปที่ gen มา วางข้างกัน
- คลิกเลือก → POST /content/draft/{id}/pick-image
Step 5 — Preview & โพสต์
- แสดง caption สุดท้าย (textarea แก้ได้) + รูปที่เลือก
- 2 ปุ่ม: "โพสต์เลย" กับ "ตั้งเวลา" (โผล่ date picker)
- POST /content/draft/{id}/post
บริบทของฉัน: [ปุ่ม/ข้อความในแต่ละ step ปรับให้ตรงภาษาทีมฉัน]
ทุก step return HTML partial ให้ HTMX swap เข้า
ใช้ Tailwind จัด style แสดงไฟล์ route ของ wizard + template ทุก step
สร้าง workers/queue.py และ workers/jobs.py — ระบบ job แบบ in-process:
workers/queue.py:
JOB_PROGRESS = {} # job_id -> {status, message, result, error}
def enqueue(job_fn, *args, job_id=None) -> str:
- gen job_id (uuid) ถ้าไม่ส่งมา
- set JOB_PROGRESS[job_id] = {status: "running", message: "starting..."}
- เริ่ม threading.Thread(target=_run_job, args=(job_id, job_fn, args))
- return job_id ทันที (non-blocking)
def get_progress(job_id) -> dict:
- return JOB_PROGRESS.get(job_id, {status: "not_found"})
def _run_job(job_id, fn, args):
- เรียก fn(*args, job_id=job_id)
- สำเร็จ: set JOB_PROGRESS[job_id] = {status: "done", result: return_value}
- exception: set {status: "error", error: str(e)}
workers/jobs.py:
def summarize_article_job(draft_id: int, url: str, job_id=None):
- update ข้อความ progress ทุก step
- fetch เนื้อบทความ (core/sources)
- classify ด้วย Haiku — ถ้า SKIP mark draft failed พร้อมเหตุผล
- summarize ด้วย Sonnet (core/summarizer)
- save ผลลง Draft
- set draft.status = "ready"
def generate_image_job(draft_id: int, tagline: str, job_id=None):
- gen รูป 2 รูป (เรียก image_maker 2 ครั้ง)
- save path ลง draft.pending_post["artwork_paths"]
- set draft.status = "ready" (กลับจาก image_gen)
def post_to_facebook_job(draft_id: int, publish_now: bool, scheduled_dt=None, job_id=None):
- โพสต์ FB (core/facebook)
- เพิ่ม comment ถ้ามี
- push LINE notification (core/line_oa)
- set draft.status = "posted", save fb_post_id
สำคัญมาก: ต้อง session.commit() ก่อน enqueue() — background thread เปิด session ของตัวเอง
ถ้า main thread ยังไม่ commit thread จะมองไม่เห็น draft
เพิ่ม route GET /api/jobs/{job_id} return JOB_PROGRESS[job_id] เป็น JSON
แสดง workers/queue.py และ workers/jobs.py แบบเต็ม
สร้าง config deploy แอป FastAPI นี้บน Linux VPS ด้วย Docker + Traefik:
1. Dockerfile:
FROM python:3.12-slim
- ติดตั้ง system deps: libjpeg-dev libpng-dev libwebp-dev libfreetype6-dev ffmpeg
- pip install จาก requirements.txt (แยก layer เพื่อ cache)
- COPY โค้ดแอป
- ENV APP_DB_PATH=/data/db.sqlite APP_STATE_DIR=/data/state
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
2. docker-compose.yml:
- service "web" build จาก .
- Volume ./data:/data (SQLite ต้อง persist ข้าม redeploy — สำคัญมาก)
- env_file: .env
- เชื่อม external Docker network "[ชื่อ network ที่ Traefik อยู่]"
- Traefik labels สำหรับ HTTPS อัตโนมัติ:
traefik.enable=true
traefik.http.routers.myapp.rule=Host(`[โดเมนของฉัน.com]`)
traefik.http.routers.myapp.tls.certresolver=mytlschallenge
traefik.http.services.myapp.loadbalancer.server.port=8000
3. .dockerignore:
.venv, __pycache__, *.pyc, .git, .env, data/, *.sqlite
4. .env.example ใส่ key ที่ต้องมีทั้งหมด (ไม่ต้องใส่ค่าจริง):
ANTHROPIC_API_KEY, OPENAI_API_KEY,
FACEBOOK_PAGE_ACCESS_TOKEN, FACEBOOK_PAGE_ID, FACEBOOK_DRY_RUN=1,
LINE_CHANNEL_ACCESS_TOKEN, LINE_GROUP_ID,
APP_DEV_MOCK_USER=[you@email.com] ← bypass auth ตอน dev
คำสั่ง deploy (รันบน VPS ผ่าน SSH):
git clone /opt/myapp && cd /opt/myapp
cp .env.example .env && nano .env # ใส่ค่าจริง
mkdir -p data
docker compose up -d --build
docker logs -f myapp-web-1 # เช็ค startup
แสดงไฟล์ทั้ง 4 แบบเต็ม