尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
เอนจิ้นเล่าเรื่องประวัติศาสตร์บนแผนที่ดาวเทียมจริง: ป้อนสคริปต์ฉาก (พิกัด/แผนกล้อง/บทพากย์) เว็บจัดกล้อง หมุด เส้นทาง เอฟเฟกต์ และเสียงพากย์จริงให้เองตามจังหวะ พาท 1 — UX/UI พื้นฐาน • เต็มจอตามขนาดจอจริงเสมอตอนแก้ไข (คอม/มือถือ/แท็บเล็ตขนาดไหนก็ได้) — ไม่ผูกสัดส่วนวิดีโอจนกว่าจะกด "บันทึกวิดีโอ" • แผงนำเข้าสคริปต์ซีน วางข้อมูลทีละแถวหรือไฟล์ Excel • เส้นเวลาฉากด้านล่าง คลิกกระโดดไปแต่ละฉาก สีบอกแผนกล้อง • หมุดไอคอน+ป้ายชื่อ, เส้นทางโค้งพร้อมหัวลูกศรระหว่างฉาก • insert-overlay (กล่องภาพลอย PiP) แยกจาก cut-to-insert (เต็มจอ) • ป้ายแบรนด์/วอเตอร์มาร์ก ตั้งชื่อย่อ+มุมได้檢視完整 README ↗
เอนจิ้นเล่าเรื่องประวัติศาสตร์บนแผนที่ดาวเทียมจริง: ป้อนสคริปต์ฉาก (พิกัด/แผนกล้อง/บทพากย์) เว็บจัดกล้อง หมุด เส้นทาง เอฟเฟกต์ และเสียงพากย์จริงให้เองตามจังหวะ
พาท 1 — UX/UI พื้นฐาน
พาท 2 — แผนที่จริง
flyTo/easeTo ตามแผนกล้อง 7 แบบ (รวม orbit หมุนกล้องจริงด้วย bearing)maplibregl.Marker และ geojson line layerพาท 3 — เสียงพากย์จริง
server.py (proxy ในเครื่อง เพราะเบราว์เซอร์เรียก edge-tts ตรงไม่ได้);; ในช่องสคริปต์ — แต่ละช่วงพากย์แยกกันแล้วเล่นต่อเนื่อง ซับไตเติลสลับตามจริงgetDisplayMedia + MediaRecorder อัดหน้าจอสด (ต้องเลือกแชร์ "แท็บนี้" ตอนเบราว์เซอร์ถาม) ได้ไฟล์ .webm — เหมาะกับดูตัวอย่างเร็วๆพาท 4 — เลือกสัดส่วนตอนส่งออก + บันทึก/โหลดโปรเจกต์ + เรนเดอร์ฝั่งเซิร์ฟเวอร์
render.py เลือกสัดส่วนแยกผ่าน --aspect 916/169 เหมือนเดิม ไม่เกี่ยวกับตัวเอดิเตอร์เลย.json (สคริปต์+แบรนด์+สัดส่วน) และ auto-save สคริปต์ล่าสุดไว้ใน localStorage ของเบราว์เซอร์ (กันรีเฟรชแล้วหาย)render.py — เรนเดอร์วิดีโอฝั่งเซิร์ฟเวอร์แบบไม่ต้องเฝ้าหน้าจอ: พากย์เสียงจริงทุกฉากล่วงหน้า วัดความยาวจริง แล้วขับ Chromium แบบ headless (Playwright) ให้เล่นตามจังหวะเสียงเป๊ะๆ อัดภาพ แล้วรวมภาพ+เสียงเป็น .mp4 ที่สมบูรณ์ — ไม่มี UI เครื่องมือแก้ไขติดไปในวิดีโอ (ต่างจากปุ่ม "บันทึกวิดีโอ" สด)insert ได้ (แทนต้องมี url รูปพร้อมอยู่แล้ว)พาท 5 — ฟอร์แมต tag ยืดหยุ่น + ไฮไลต์เขตแดนจริง + กล้องเอียง/หมุน
key=value แบบไม่ต้องเรียงลำดับ ควบคู่กับฟอร์แมต column เดิม (ใช้แบบไหนก็ได้ แม้ในไฟล์เดียวกัน) — พิมพ์เฉพาะคีย์ที่จำเป็น ที่เหลือ default ให้tilt= (0-60°) และ bearing= (0-360°) ปรับมุมเอียง/ทิศกล้องต่อฉากได้ตรงๆ แทนที่จะ fix pitch=0 เสมอ — ให้ภาพมุมกล้อง 3D แบบในคลิปตัวอย่าง (Animaps/MapRecap)highlight=country/province/place ไฮไลต์เขตแดนจริงจาก OpenStreetMap (Nominatim reverse geocoding ตรงจากพิกัดฉาก ไม่ใช่ค้นชื่อ จึงแม่นยำ) พร้อมป้ายชื่อเขตขึ้นกลางจอแบบเคลื่อนไหวพาท 6 — ลูกเล่นแบบ AnimateMyMap: War Morph, fill reveal, border trace หลายทิศ, ค้นหาสถานที่
warmorph=RRGGBB:lat1,lng1:lat2,lng2 — พื้นที่ยึดครองค่อยๆเปลี่ยนสีจากฝ่ายแพ้เป็นฝ่ายชนะจริง โดยตัด polygon จริงด้วยเส้นที่กำหนด (ไม่ใช่แค่วาดทับ) ใช้คู่กับ highlightreveal=fade/wipe/split/circular/diamond — รูปแบบการเผยพื้นที่ไฮไลต์ ตัด polygon จริงด้วยหน้ากากที่โตขึ้นตามเวลา (ใช้เทคนิคเดียวกับ warmorph)trace=one/two/tworeverse/four — จำนวนจุดเริ่มลากเส้นขอบเขตพร้อมกัน + เพิ่ม glow ใต้เส้นmainland=on — ตัดเกาะ/ดินแดนไกลออกไปทิ้งตอนคำนวณกรอบกล้องอัตโนมัติ (เช่น ฝรั่งเศสไม่รวมเฟรนช์เกียนา)labelfont / labelsize / labelweight — ปรับฟอนต์/ขนาด/น้ำหนักตัวอักษรป้ายชื่อหมุดเฉพาะฉาก/api/geocode ผ่าน Nominatim forward search) พิมพ์ชื่อแล้วคลิกผลลัพธ์เพื่อแทรกแถวพร้อมพิกัดจริงลงสคริปต์ทันที ไม่ต้องรู้ lat,lng เองแผนกล้อง 7 แบบ: establishing (ภาพเปิดกว้าง) · fly-to (กล้องบินเดินทาง) · push-in (ซูมเข้าถึงจุดหมาย) · zoom-out (ซูมออกเผยภาพรวม) · orbit (กล้องหมุนรอบจุดสนใจ) · cut-to-insert (ตัดเข้าภาพเต็มจอ) · insert-overlay (แทรกภาพลอย PiP)
พาท 7 — แก้อนิเมชันกระตุก ให้ลื่นไหลขึ้น
highlight + กล้องแบบ auto-frame (establishing/fly-to/push-in/zoom-out) จะมีกล้องขยับ 2 รอบซ้อนกัน — รอบแรกจาก moveCamera() ไปที่พิกัดหมุดทันที รอบสองจาก showBoundary() ไปที่กรอบเขตแดนจริงตอนโหลดเสร็จ (มาช้ากว่าเพราะต้องรอ Nominatim) ทำให้กล้องกระตุกเหมือนสะดุดกลางอากาศ — แก้โดยให้ showBoundary() เป็นเจ้าของการขยับกล้องเพียงจุดเดียวสำหรับฉากประเภทนี้easeInOutCubic) ให้ทุกอนิเมชันที่ขับด้วย requestAnimationFrame เอง (ลากเส้นขอบเขต, เผยพื้นที่ไฮไลต์, war morph, ไอคอนพาหนะวิ่งตามเส้นทาง) แทนความเร็วคงที่แบบเส้นตรง — เริ่ม/จบนุ่มนวลกว่าเดิมพาท 8 — ตัดเส้นไฮไลต์ให้อยู่แค่บนแผ่นดิน + เครื่องมือจาก AnimateMyMap Studio
data/land_mask_50m.geojson, ผ่าน shapely ฝั่ง server.py) ก่อนส่งให้หน้าเว็บเสมอ — ผลลัพธ์คือเส้นไฮไลต์แนบชายฝั่งจริงเหมือน AnimateMyMap โดยไม่ต้องพึ่งความแม่นยำของ mapper แต่ละประเทศใน OSMcaption=ข้อความ | captionpos=top/center/bottom — ข้อความลอยกลางจอ ไม่ผูกกับหมุด สำหรับทำหัวข้อ/คำโปรยแยกจากซับไตเติลบทพากย์ (เทียบเท่า "Add Text")geophoto=url:lat,lng — ปักรูปจริง (จาก URL ที่อัปโหลดไว้แล้ว) ลงตามพิกัดจริงบนแผนที่ เป็นการ์ดรูปเด้งเข้า (เทียบเท่า "Add Image")draw=lat,lng;lat,lng;... | drawcolor=RRGGBB — ลากเส้นอิสระตามพิกัดที่พิมพ์เอง ใช้ขีดเส้นทาง/วงล้อม/เน้นจุดที่ไม่ใช่ขอบเขตทางการ (เทียบเท่า "Pen" แบบพิมพ์พิกัดแทนลากเมาส์ เพราะเอนจิ้นนี้เป็นระบบสคริปต์ ไม่ใช่ตัวแก้ไขด้วยเมาส์)persist=N — ให้เขตแดนที่ไฮไลต์ไว้ (พร้อมป้ายชื่อ) ค้างอยู่ต่ออีก N ฉากถัดไปแม้ฉากนั้นไม่ได้ตั้ง highlight เอง ใช้เล่าเรื่องต่อเนื่องในพื้นที่เดิมหลายจังหวะโดยไม่ต้องไฮไลต์ซ้ำ (เทียบเท่า "Keep this group visible")landfill=flag ที่มีอยู่แล้ว, "Pen" ของเขาเป็นฟีเจอร์ปิด (ต้องอัปเกรดแพลน) จึงยังไม่มีอ้างอิงเทียบให้แน่ชัด — เพิ่ม draw= ให้เป็นเวอร์ชันของเราไปก่อนตามแนวคิดเดียวกันพาท 9 — ปรับแต่งก่อนอัด (เสียง/เพลง/จังหวะ/สไตล์) + จัดระเบียบแผงนำเข้าสคริปต์
/api/tts (ตรวจรูปแบบค่าก่อนส่งเข้า edge_tts.Communicate เสมอกันค่าผิดรูปแบบหลุดเข้าไป)render.py --bgm ไฟล์.mp3 --bgm-volume 0.25) จะ mix รวมเข้ากับเสียงพากย์จริงด้วย ffmpeg amix ก่อนรวมเป็นวิดีโอสุดท้ายsceneGap): พักเงียบระหว่างฉากได้ 0-3 วินาที กันเนื้อหาแน่นเกินไปไม่มีจังหวะหายใจ — render.py --gap วินาที แทรกช่วงเงียบจริงในแทร็กเสียงให้ตรงกับที่ browser หยุดรอเช่นกันrender.py --subpos/--subcolor/--subsize/--subweight ให้ตรงกันตอนเรนเดอร์คลิปยาว<details> แยกจากกัน ไม่กางค้างเต็มพื้นที่เหมือนเดิม — เห็นแค่ช่องพิมพ์สคริปต์ + ปุ่ม "แปลงเป็นฉาก" ทันทีที่เปิดแผง, ปุ่มแปลง/ล้างลอยติดขอบล่างเสมอ (sticky) และรายการฉากที่แปลงแล้วจำกัดความสูงมีสกอลของตัวเอง — แก้ปัญหาสคริปต์ยาวๆแล้วต้องเลื่อนเมาส์ไกลกว่าจะเจอปุ่มhide=topbar,timeline ล็อกปุ่มเล่นตอนแก้ไข: เดิมถ้าฉากไหนตั้ง hide=topbar (สำหรับให้วิดีโอที่อัดออกมาไม่ติด UI) มันจะซ่อนแถบควบคุมทันทีแม้ตอนแก้ไข/พรีวิวในเบราว์เซอร์ ทำให้กดเล่น/หยุดไม่ได้อีกเลยถ้าฉากนั้นเป็นฉากที่กำลังเปิดอยู่ — แก้ให้ hide=topbar/timeline มีผลจริงเฉพาะตอนกด "บันทึกวิดีโอ" หรือตอน render.py อัดจริงเท่านั้น ตอนแก้ไขปกติจะเห็นแถบควบคุมเสมอพาท 10 — กล้อง fly-to ซูมตามระยะทางจริง, ไม่ดีฟอลต์ไอคอนพาหนะผิดยุค, พากย์เสียงทั้งคลิปให้เสร็จก่อนเล่น
cam=fly-to ซูมตามเนื้อหาจริง: เดิม fly-to ใช้ zoom 6.2 ตายตัวเสมอ ทำให้จุดสองจุดที่ใกล้กันมากๆ (เช่นในจังหวัดเดียวกัน) กล้องยังถอยไกลเกินพื้นที่จริงเห็นแค่จุดเล็กๆกลางจอ — แก้ให้คำนวณกรอบกล้องจาก map.cameraForBounds ครอบทั้งจุดต้นทาง-ปลายทางจริง (จำกัดช่วง zoom 3-10 กันจุดใกล้กันมากๆซูมเข้าจนแปลก) ใช้เฉพาะตอนไม่มี highlight (ถ้ามี highlight ให้ showBoundary() เป็นเจ้าของกล้องเหมือนเดิม)transport= เดิมดีฟอลต์เป็น plane ทำให้ฉากประวัติศาสตร์ก่อนยุคเครื่องบินมีเครื่องบินบินให้เห็นอย่างผิดยุค — ตอนนี้ไม่ระบุ transport= แปลว่าไม่มีไอคอนวิ่งบนเส้นทางเลย (มีแต่เส้นทาง+ลูกศร) ต้องระบุเองถ้าต้องการไอคอนพาหนะพาท 11 — ลูกเล่นสไตล์ History Toon Lab / Whyhistory: โฟกัสขาวดำ, สีไฮไลต์เอง, ป้ายวงกลม, กล่องแทรกมีเส้นโยง
focus=on: ทำพื้นหลังทั้งจอเป็นขาวดำ (ปรับ raster-saturation ของเลเยอร์ภาพถ่ายดาวเทียมเป็น -1) เหลือแค่เขตที่ไฮไลต์เป็นสีสดจัด (ฟิลด์ทึบขึ้นเป็น ~0.78 จากปกติ 0.18) ให้ความรู้สึกแบบคลิป Whyhistory ที่ไฮไลต์ประเทศเดียวเด่นออกมาจากพื้นหลัง ต้องใช้คู่กับ highlighthighlightcolor=RRGGBB: เปลี่ยนสีไฮไลต์เขตแดน/เส้นขอบ/glow จากเหลืองทองดีฟอลต์เป็นสีที่ต้องการ (เช่นให้แต่ละประเทศในซีรีส์เดียวกันมีสีเป็นเอกลักษณ์ของตัวเอง)badge=flag:xx:lat,lng:ป้ายชื่อ หรือ badge=icon:🏛️:lat,lng:ป้ายชื่อ: ไอคอนวงกลม (ธงชาติจริงผ่าน /api/flag หรืออีโมจิ) พร้อมป้ายชื่อปักตามพิกัดจริง เทียบเท่าไอคอนวงกลม+ป้ายชื่อของ Whyhistorycallout=url:lat,lng:ป้ายชื่อ: วงแหวนสีทองชี้จุดพิกัดจริงบนแผนที่ พร้อมเส้นโยงเรืองแสงไปยังกล่องรูป/ภาพแทรกมุมขวาบน (ตำแหน่งวงแหวนคำนวณสดทุกเฟรมจาก map.project() เหมือนกลไก shade spotlight เดิม เส้นโยงเลยขยับตามกล้องได้จริง) เทียบเท่าการ zoom-in ชี้จุดพร้อมภาพหลักฐานแทรกของคลิปไทย-กัมพูชาพาท 12 — ความแม่นยำเขตแดนทั่วโลก + สวิตช์ตัดชายฝั่ง
country=4, province=6, place=10) ซึ่งคาลิเบรตจากลำดับชั้นการปกครองไทยเป็นหลัก พอใช้กับประเทศอื่นที่โครงสร้างต่างกันเลยได้ระดับผิด — ตอนนี้ไล่ยิงตาม "บันไดค่า zoom" ต่อระดับ แล้วตรวจ addresstype ที่ Nominatim ตอบกลับมาจริงว่าตรงระดับที่ขอไหม แบ่งเป็นชนิดหลัก (เจอแล้วใช้ทันที) กับชนิดสำรอง (ใช้ต่อเมื่อไล่จนหมดแล้วไม่เจอชนิดหลัก)city (เดิมได้ suburb = เขตย่อยไม่ใช่ตัวเมือง), โตเกียวได้ city, ซิดนีย์ระดับ province ได้ state = New South Wales, กรุงเทพระดับ place ได้เขตพระนคร (suburb ซึ่งเป็นระดับเขตจริงของ กทม. ใน OSM)data/land_mask_10m.geojson, ~10MB) ชายฝั่งละเอียดขึ้นประมาณ 10 เท่า — ไฟล์ 10m เก็บทวีปเป็น MultiPolygon ก้อนใหญ่ จึงต้องแตกเป็นรูปหลายเหลี่ยมเดี่ยว 6,837 ชิ้นตอนโหลด ไม่งั้นการกรองด้วย bbox ไม่ช่วยอะไรและตัดช้ามากlandclip=0/1 ได้พาท 13 — แผงนำเข้าสคริปต์ยื่นเต็มจอ, ปุ่มควบคุมย้ายมุมซ้ายบน, แก้ browser cache ค้างโค้ดเก่า
server.py เดิมเสิร์ฟ script.js/styles.css แบบไฟล์ static ธรรมดา ไม่มี header กัน cache และไม่มี version ต่อท้าย URL — เบราว์เซอร์บางตัวจะจำ response เก่าไว้แบบไม่ยอมเช็กซ้ำเลยแม้รีสตาร์ตเซิร์ฟเวอร์ ทำให้แก้โค้ดแล้วเหมือนไม่มีผล (ต้องล้าง cache มือทุกครั้ง) — แก้โดยเสิร์ฟ index.html เองพร้อมแปะ query ?v=<mtime ไฟล์จริง> ต่อท้าย script.js/styles.css อัตโนมัติทุกครั้งที่มีการแก้ไข บวกส่ง Cache-Control: no-store ทุก response กัน browser cache ใดๆค้างอีกพาท 14 — "สร้างฉากง่ายๆ" (จากจุด A ไป B) + เส้นทางวิ่งตามถนนจริง
route=road: เส้นทางระหว่างฉากก่อนหน้ากับฉากนี้วิ่งตามถนนจริง (เรียก OSRM demo server สาธารณะฟรี ไม่ต้องมีคีย์ ผ่าน /api/route ที่ proxy ให้) แทนเส้นโค้งจำลองเดิม — วาดเส้นโค้งจำลองไปก่อนทันที (กันหน้าจอค้างรอเน็ต) แล้วสลับเป็นเส้นถนนจริงเมื่อโหลดเสร็จ ไอคอนพาหนะก็วิ่งตามเส้นถนนจริงนี้ด้วยถ้าตั้ง transport=pointAtFraction) รองรับทั้งเส้นโค้งจำลองเดิมและเส้นถนนจริงที่มีเป็นร้อยจุดในโค้ดเดียวกันพาท 15 — เส้นประวิ่ง, กล้องตามพาหนะ, ความเร็วเดินทางจริง + ขัดความสวยงาม 3 จุด
scene-line-layer) ตอนนี้ไล่สลับ line-dasharray ต่อเนื่องตลอดเวลา (marching ants) แทนเส้นประนิ่งๆเดิม เพราะ MapLibre ไม่มี line-dashoffset ให้ใช้ตรงๆfollow=on: กล้องไล่ตามไอคอนพาหนะไปตลอดเส้นทาง (ซูมค้างที่ระดับ "ติดตาม" ~12 ไม่ใช่ซูมเห็นเส้นทางทั้งหมดนิ่งๆแบบเดิม) ใช้กับ cam=fly-to — แก้บั๊กระหว่างพัฒนา: เดิม map.jumpTo({center}) ที่ขับกล้องทุกเฟรมไปขัดจังหวะ easeTo ที่ตั้งซูมไว้ตอนต้นฉาก ทำให้ซูมค้างกลางทางไม่ถึงเป้าหมาย ต้องระบุ zoom ชัดเจนทุกครั้งที่ jumpTo ด้วยspeed=กม./ชม.: คำนวณเวลาไอคอนเดินทางจากระยะทางจริง/ความเร็วที่ตั้ง ให้ฟีลเร็ว-ช้าสมจริงตามพาหนะ (เครื่องบินเร็วกว่าเดินเท้า) — แก้บั๊กสำคัญ: ถ้าคำนวณตรงๆจะได้เวลาที่ไกลๆกลายเป็นหลักสิบนาทีจริงบนจอ (ทางไกล+ความเร็วต่ำ) จึงจำกัดไม่ให้เกินความยาวฉากที่มีจริงเสมอ (speed มีผลแค่ทำให้ไอคอนไปถึงเร็วกว่าความยาวฉากได้ ไม่ทำให้ฉากยาวขึ้น เพราะเสียงพากย์เป็นตัวกำหนดความยาวฉากจริงอยู่แล้ว)route=road, follow, speed เพิ่มในแผงสร้างฉากง่ายๆด้วย ไม่ต้องพิมพ์ tag เองพาท 16 — แก้ไขฉากที่สร้างไว้ก่อนหน้าได้ (เหมือนตัดต่อวิดีโอ)
sceneRawLines[i] ตรงกับ scenes[i] เป๊ะ กรองบรรทัดที่ parse ไม่ผ่านออกพร้อมกันทั้งคู่ กัน index เพี้ยน) ตอนบันทึกแก้ไขจะประกอบสคริปต์ใหม่จากบรรทัดที่ parse ได้จริงทั้งหมดพาท 17 — พรีเซ็ตสไตล์นำเสนอ 30 แบบ (รวมหมวดผี/ลึกลับใหม่), ค้นภาพประกอบอัตโนมัติ, พากย์เสียงเป็นธรรมชาติขึ้น, กล้องลื่นไหลขึ้น
style=: พรีเซ็ต 30 แบบใน 5 หมวด (สารคดี/วล็อกเดินทาง/ข่าว/มหากาพย์-สงคราม/ผี-ลึกลับ ← หมวดใหม่) แต่ละพรีเซ็ตตั้ง effect (สติ๊กเกอร์ธีม เช่น 👻🕯️🌫️⚡🏚️👑 ที่เพิ่มเข้า EFFECT_GLYPHS), highlightcolor, shade (สปอตไลต์), reveal, captionpos, และ โทนพากย์เสียง (rate/pitch ของ edge-tts) ให้อัตโนมัติ — ทับเฉพาะฟิลด์ที่ผู้ใช้ไม่ได้ตั้งเองใน tag อื่น ไม่ยุ่งกับ cam/highlight ที่เลือกเอง เลือกได้จากดรอปดาวน์ในแผง "สร้างฉากง่ายๆ" หรือพิมพ์ tag เอง/api/imagesearch (server.py) แล้วเติม geophoto=url:lat,lng:ป้ายชื่อ ให้อัตโนมัติgeophoto: เดิมรับแค่ url:lat,lng ใส่ป้ายชื่อต่อท้ายไม่ได้ (parse ล้มเหลวเงียบๆ) — ตอนนี้รับ url:lat,lng:ป้ายชื่อ ได้แบบเดียวกับ badge/callout แล้ว (ป้ายใส่หรือไม่ใส่ก็ได้)style= มีโทนพากย์ของตัวเอง (เช่นหมวดผีพูดช้าลง-เสียงต่ำลงกว่าปกติ, วล็อกพูดไวขึ้น-เสียงสูงขึ้นเล็กน้อย) (2) เพิ่มช่วงหายใจสั้นๆ (180ms) ระหว่างประโยคที่แบ่งด้วย ;; ในฉากเดียวกัน กันเสียงพากย์รัวติดกันไม่เป็นธรรมชาติeaseTo/flyTo ใช้ easing แบบ easeInOutCubic ร่วมกัน (EASE_CINEMATIC) แทนความเร็วคงที่เดิม กันกล้องกระชากตอนเริ่ม/หยุดขยับgeophoto) และป้ายวงกลม (badge) ที่เคยไปทับป้ายชื่อหมุดหลัก (pin label) เวลาอยู่พิกัดเดียวกัน — ยกมาร์กเกอร์ทั้งสองขึ้น 34px ด้วย offsetพาท 18 — Story System: narrative / beat / importance / mood + auto narrative analysis (เพิ่มความสามารถล้วนๆ ไม่กระทบของเดิม)
narrative/beat/mood เป็น auto, importance เป็น medium — สคริปต์เดิมที่ไม่มี 4 คีย์นี้ทำงานเหมือนเดิม 100% (ตรวจแล้ว: camPaceMul=1, holdBonusMs=0, styleMood=null เท่าของเดิมทุกประการ)narrative: hook/context/setup/development/escalation/conflict/reveal/turningpoint/climax/consequence/resolution/conclusion — หน้าที่ของฉากในเรื่อง เป็น metadata สำหรับ auto-analysis + แสดงในพรีวิว ไม่บังคับพฤติกรรมกล้อง/เอฟเฟกต์เองbeat: hook/question/setup/reveal/escalation/conflict/twist/turningpoint/payoff — จังหวะสำคัญของการเล่าเรื่อง; reveal/twist/turningpoint/payoff เสริมเวลาพักก่อนตัดฉากถัดไปให้อัตโนมัติ (holdBonusMs)importance: low/medium/high/critical — คุมแค่ "จังหวะ" (ความเร็วกล้องที่มีอยู่แล้ว + เวลาพักก่อนตัดฉาก) เท่านั้น ไม่เพิ่ม effect/reveal/camera-movement ใหม่ให้เอง ตามที่กำหนดไว้ — critical กล้องขยับช้าลง 30% + พักก่อนตัดฉาก +900ms, high ช้าลง 15%+400ms, low ไวขึ้น 20%, medium (ดีฟอลต์) ไม่เปลี่ยนอะไรเลยmood: calm/curious/mysterious/tension/fear/tragic/epic/hope/shock — คุมจังหวะกล้อง (คูณเพิ่มจาก importance) + โทนพากย์เสียง (rate/pitch) เท่านั้น ผู้ใช้ตั้งเองทับ style= preset's mood เสมอ, ไม่ตั้งแต่มี style= ใช้โทนของ style เดิม (ไม่เปลี่ยนพฤติกรรมเดิม), ไม่มีทั้งคู่ engine เดาจากเนื้อหาเป็นโทนแนะนำเบาๆ"กองทัพเยอรมันบุกโปแลนด์" → narrative=conflict, beat=conflict, importance=high (ทดสอบแล้วตรงตามตัวอย่างที่กำหนด) — เดาไม่ออกจากคำในสคริปต์ ลองเดาจาก cam=battle-map/effect=battle,fire,lightning ต่อ เดาไม่ออกเลยตกไปที่ดีฟอลต์ปลอดภัย ผู้ใช้ระบุเองทับค่าเดาเสมอไม่ว่าฟิลด์ไหนmood= ที่ผู้ใช้ตั้งเอง > โทนจาก style= preset (เดิม) > โทนที่ auto เดาจากเนื้อหา > ไม่มีอะไรเลย (ใช้ค่าเสียงกลางปกติ) — ทั้งหมดยังไหลผ่านฟิลด์ styleMood เดิม ไม่ได้เปลี่ยนจุดเชื่อมกับระบบพากย์เสียงเลยพาท 19 — Camera System: shot / cameraaction / focuspoint / motion / motioncurve + Geographic Camera Continuity (เพิ่มความสามารถล้วนๆ ไม่กระทบของเดิม)
cam เดิมทั้ง 8 แบบ (establishing/fly-to/push-in/zoom-out/orbit/cut-to-insert/insert-overlay/battle-map) ยังทำงานเหมือนเดิมทุกตัว — ตรวจแล้ว: สคริปต์ที่ไม่มีคีย์ใหม่พวกนี้เลยได้ shotZoomOverride=null, camActionMotionMul=1 = ไม่กระทบเลขจริงใดๆ เลย (คีย์ใหม่ resolve เป็น label ดีฟอลต์ตาม cam ไว้แค่โชว์พรีวิว)shot: establishing/wide/medium/close/detail/insert — ระยะภาพ ปรับซูมจาก baseline เดิมของ cam นั้นๆ เฉพาะตอนผู้ใช้ระบุเอง เท่านั้นcameraaction: static/gentle/cinematic/dynamic/urgent/tracking/reveal/drift — ลักษณะการเคลื่อนกล้อง คูณเข้ากับความเร็วกล้อง (0.65×-1.4×)motion: static/slow/normal/fast/accelerate/decelerate — ความเร็วกล้อง คูณรวมกับ cameraaction; accelerate/decelerate เซ็ต motioncurve โดยนัยให้ (ease-in/ease-out) ถ้ายังไม่ได้ตั้งเองmotioncurve: linear/ease-in/ease-out/ease-in-out — รูปแบบเร่ง/ชะลอ แทน easing เริ่มต้นของระบบ (EASE_CINEMATIC) เฉพาะตอนตั้งเองfocuspoint: focuspoint=lat,lng — แยกจุดที่กล้องเล็งออกจากพิกัดหลักของฉากได้ (latlng ยังเป็นพิกัดของหมุด/ไฮไลต์/มาร์กเกอร์อื่นทุกจุดเหมือนเดิม ไม่กระทบ)tilt=/bearing= เอง และพิกัดอยู่ใกล้ฉากก่อนหน้า (< 60กม.) กล้องจะสืบทอดมุมเอียง/ทิศจากฉากก่อนแทนการรีเซ็ตกลับ 0 — ทดสอบแล้ว: ฉากใกล้กันสืบทอด pitch/bearing ถูกต้อง, ฉากไกลกัน (คนละทวีป) รีเซ็ตกลับ 0 ตามพฤติกรรมเดิม; ตั้ง tilt=/bearing= เองเมื่อไหร่ก็ทับค่าสืบทอดเสมอ (ครอบคลุมเฉพาะ path หลักใน moveCamera(), ไม่รวม path ไฮไลต์เขตแดนที่ showBoundary() คุมกล้องเอง)พาท 20 — Scene Connection System: continuity / visualbridge / transition / returnmap (เพิ่มความสามารถล้วนๆ ไม่กระทบของเดิม)
continuity="auto" (heuristic ระยะทางเดิม), visualBridge=false, transition=null→"none", returnMap=false = พฤติกรรมเดิม 100%continuity: on/off — ทับ heuristic ระยะทางเดิมเสมอ (on สืบทอด position/zoom/tilt/bearing ไม่ว่าไกลแค่ไหน, off รีเซ็ตทุกครั้งแม้ใกล้ฉากก่อนหน้า) — ทดสอบแล้ว: off กับฉากใกล้กันรีเซ็ตกลับ 0/0 จริง, on กับฉากไกลกันคนละทวีปสืบทอด pitch/bearing/ซูมถูกต้อง (เพิ่มการสืบทอดซูมจากพาท 19 ที่ยังไม่รวม)visualbridge: on — ฉากที่ไม่ใช่ fly-to ถ้ากระโดดไกลจากฉากก่อนหน้ามาก (≥60กม. ใช้ threshold เดียวกับ continuity) จะซูมออกเห็นทั้งสองจุดสั้นๆ (500ms) ก่อนแล้วค่อยเข้ากล้องจริงของฉากนั้นต่อ (setTimeout ต่อคิว กัน easeTo สองอันชนกันกลางอากาศ) แทนตัดตรงแบบ "รีเซ็ตแผนที่เริ่มใหม่" ตามตัวอย่างในสเปค (เบอร์ลิน→ฝรั่งเศส)transition: cut/fade/dissolve/match/travel/morph/reveal/whip/none — แฟลชสั้นๆ (CSS keyframe ผ่าน #transitionOverlay, ไม่บล็อก JS) กลบรอยตัดฉาก ไม่ระบุ = engine เดาเอง: แผนที่→แผนที่ปกติ = none (ไม่มีอะไรเลย กัน "ห้ามใช้ transition ทุกฉาก"), แผนที่↔ภาพแทรกจริง (cut-to-insert/insert-overlay) = dissolve นุ่มๆอัตโนมัติ, beat=twist/turningpoint = cut เน้นจังหวะ — importance สูงลดความหวือหวาอัตโนมัติ (ทดสอบแล้ว: dissolve ปกติ opacity 0.5, importance=critical เหลือ 0.15)returnmap: on — ติดตาม "สถานะกล้องแผนที่จริงล่าสุด" แยกจากสถานะกล้องทั่วไป (ข้าม insert scene ที่ไม่ใช่ตำแหน่งกล้องแผนที่จริง) ฉากที่กลับมาเป็นแผนที่จะสืบทอด zoom/tilt/bearing จากจุดนั้นแทนเริ่มใหม่ แม้พิกัดจริงของฉากนั้นจะอยู่ไกลคนละทวีปก็ตาม (ทดสอบแล้ว: A push-in zoom10/tilt25 → B insert (คนละทวีป) → C returnmap=on ที่ญี่ปุ่น ได้ zoom10/tilt25 กลับมาถูกต้อง)พาท 21 — Map System: mapmode (เพิ่มความสามารถล้วนๆ ไม่กระทบเครื่องมือแผนที่เดิมเลยแม้แต่ตัวเดียว)
highlight/arrows/warmorph/draw/persist/route/follow/speed/shade/hide/landfill/reveal/trace/mainland/focus/highlightcolor/badge/callout/geophoto/timeline — ไม่ถูกแตะเลยแม้แต่บรรทัดเดียว ตรวจแล้ว: ฉากที่ใช้เครื่องมือเดิมครบ 15 ตัวพร้อมกัน (arrows/warmorph/draw/badge/callout/geophoto/highlight/route/persist/shade/reveal/trace/mainland/focus/highlightcolor) resolve ค่าถูกต้องทุกตัวเหมือนเดิมเป๊ะmapmode: location/journey/movement/expansion/retreat/encirclement/invasion/territorial-change/comparison/before-after/timeline — ป้ายกำกับ "โหมดแผนที่" ของฉาก เป็น metadata ล้วนๆ (คล้าย narrative) ไม่เพิ่ม arrows/highlight/draw/warmorph/route ให้เองเด็ดขาด ตามที่ห้ามไว้ — ผู้ใช้ระบุเองเป็น priority สูงสุดเสมอwarmorph→territorial-change, arrows→movement, route=road→journey, draw→encirclement, narrative=conflict/escalation→invasion) ไม่มีสัญญาณอะไรเลย = location (ดีฟอลต์กลาง) — ทดสอบตรงตามตัวอย่างในสเปคทั้ง 3 ข้อ: "กองทัพเคลื่อนเข้าสู่กรุงปารีส" → movement, "จักรวรรดิขยายอำนาจไปทั่วทะเลเมดิเตอร์เรเนียน" → expansion, "กองทัพถูกล้อมที่เมืองสตาลินกราด" → encirclementพาท 22 — Insert System: insertmode / inserttransition / evidence + ระบบป้องกันการบัง (เพิ่มความสามารถล้วนๆ ไม่กระทบ insert เดิม)
insert= (ข้อความ/URL รูป/รูปอัปโหลด) ยังทำงานเหมือนเดิมทุกจุด ต้องมี cam=cut-to-insert/insert-overlay อยู่ก่อนถึงจะโชว์อะไรเลย — คีย์ใหม่ 3 ตัวไม่มีผลถ้าฉากไม่มี insert= ไม่บังคับให้ทุกฉากต้องมี insert ตามที่ห้ามไว้; geophoto=/callout= ไม่ถูกแตะเลย รักษาพฤติกรรมเดิม 100%insertmode: full/overlay/picture-in-picture/split/background — เลย์เอาต์กล่อง insert ไม่ระบุ = ดีฟอลต์ตาม cam เดิม (cut-to-insert→full, insert-overlay→picture-in-picture) ตรวจแล้ว: ดีฟอลต์ทั้งสอง cam ให้ CSS เหมือนเดิมเป๊ะ (กฎ CSS ใหม่แต่ละอันจงใจเล็งไปที่ element ฝั่งที่ยังซ่อนอยู่สำหรับกรณีดีฟอลต์ จึงไม่มีผลทางสายตาเลย) split/background/overlay คือเลย์เอาต์ใหม่จริง ต้องระบุเองถึงจะเห็นinserttransition: cut/fade/dissolve/match/slide/flash — มีผลเฉพาะจังหวะเข้า/ออก insert เท่านั้น (Map↔Historical Evidence) ทับดีฟอลต์ dissolve อัตโนมัติของพาท 20 แต่ยังแพ้ transition= ทั่วไปถ้าตั้งไว้ด้วย — เพิ่ม 2 แบบใหม่ในตัวเล่นเปลี่ยนฉากเดิม (slide กวาดแถบขาว/ดำผ่านจอ, flash แฟลชขาว) ผ่าน #transitionOverlay ตัวเดิมevidence: photo/painting/document/newspaper/portrait/artifact/archive/map — ป้าย label บอกประเภทหลักฐาน (แสดงเป็นข้อความไทยมุมกล่อง insert) ห้ามสร้าง/เติม URL เองเด็ดขาด ตามที่กำหนด — โชว์เฉพาะตอนมี insert= จริงและระบุ evidence= เอง (ไม่มี = ไม่โชว์อะไรเลย)callout=/badge= ด้วย กล่อง overlay/PiP จะสลับไปฝั่งตรงข้ามอัตโนมัติ (insert-flip) กันชนกล่อง callout เดิม — ทดสอบแล้ว: ฉากมี callout ได้ class insert-flip ถูกต้อง (ขอบเขตที่ทำจริง: ป้องกันระดับโครงสร้าง/เลย์เอาต์ ไม่ใช่ real-time pixel-collision กับหมุด/ลูกศรที่กำลังเคลื่อนไหวบนจอ)พาท 23 — Documentary Editor Intelligence: pace/pacing/density/attention/emphasis/intensity/composition + Auto Documentary Rules (layer บนของทุกระบบเดิม)
camPaceMul/holdBonusMs/transition-peak ที่มีอยู่แล้วเท่านั้น ตรวจแล้ว: สคริปต์ไม่มี 7 คีย์นี้เลยได้ docPaceMul=1, holdBonusMs=0(ไม่บวกเพิ่ม), effectSuppressed=false = พฤติกรรมเดิม 100%pace: very-slow/slow/normal/fast/very-fast — ความไวกล้อง (1.5×-0.55×) narrative=climax เดาเป็น fast อัตโนมัติ (กฎ 11)pacing: build/accelerate/decelerate/release/pause — จังหวะรวม release/pause เสริมเวลาพักก่อนตัดฉาก — หลัง narrative=climax ฉากถัดไปได้ pacing=release อัตโนมัติถ้ายังไม่ตั้งเอง (กฎ 12, เป็น post-process หลัง parse ทั้งสคริปต์เพราะต้องรู้จักฉากก่อนหน้า) — ทดสอบแล้ว: ฉากหลัง climax ได้ docPaceMul ช้าลง+holdBonusMs=500 ถูกต้องdensity: minimal/low/medium/high — ดีฟอลต์ medium; high ลดความเร็วกล้อง (กฎ 4) + เพิ่มเวลาอ่าน + ลด effect ที่มาจาก style preset เท่านั้น ไม่แตะ effect= ที่พิมพ์เองattention: primary/secondary/background — ดีฟอลต์ primary; background ลดความเด่นซับไตเติล (opacity 0.72) + ลด effect ที่มาจาก style preset เหมือน density=highemphasis: none/subtle/medium/strong — คูณเข้ากับ transition ที่ resolve ไว้แล้วเท่านั้น ไม่สร้าง transition เองจากศูนย์ (กันเกินตามกฎ 10) ดีฟอลต์ medium (คูณ 1 ไม่เปลี่ยนอะไร ไม่ใช่ "none" เพื่อกัน dissolve อัตโนมัติของ insert หายไปเงียบๆ) beat=twist/turningpoint เดาเป็น subtle อัตโนมัติ เพดานสูงสุด strong=1.5× ตามที่ห้าม "มากเกินไป"intensity: low/medium/high — ความเข้มข้นกล้อง ดีฟอลต์ medium; climax เดาเป็น high อัตโนมัติ (กฎ 11)composition: auto/left/right/center/top/bottom — left/right ทับ heuristic ป้องกันการบัง insert อัตโนมัติของพาท 22 ได้โดยตรงeffectSource==="user" ไม่ถูกแตะเด็ดขาดไม่ว่ากรณีไหน) — ทดสอบแล้ว: ฉาก style+insert+caption+arrows+highlight (5 อย่าง) + density=high ได้ effectSuppressed=true แต่ฉากที่พิมพ์ effect=fire เองได้ effectRender="fire" เหมือนเดิมเป๊ะพาท 24 — Global Compatibility Rules: Internal Validation + Debug Mode (ตรวจสอบย้อนหลังทุกระบบ พาท 18-23)
xxxValid || guess.xxx || default ทุกจุด) — ตรวจย้อนหลังผ่านแล้วทุกคีย์ใหม่ทั้ง 30+ ตัวparseTagRow ดั้งเดิม (if (!key) continue;) — enum ผิดทุกคีย์ใหม่ตกไปดีฟอลต์ปลอดภัยเสมอ (ไม่มีคีย์ไหน throw หรือทำให้ฉากอื่น render พังไปด้วย) ตรวจแล้ว: ฉากที่มี unknownkey=xyz render ฉากได้ปกติทุกจุดvalidateScript()/validateTagLine() ตรวจ: จำนวนบรรทัด, place/cam/script ขาด, cam ไม่รู้จัก, latlng ผิดรูปแบบ, ;; ว่างเปล่าใน script, คีย์ไม่รู้จัก (info level), column-mode น้อยกว่า 4 คอลัมน์ — ทำงานทุกครั้งที่กด "แปลงเป็นฉาก" แต่ไม่แสดงอะไรเลยใน Production Mode (ดีฟอลต์) ต้องเปิด "โหมดตรวจสอบ (Debug)" เองถึงจะเห็นรายงาน — ทดสอบแล้ว: สคริปต์ 5 บรรทัดมีปัญหา 3 แบบ (ขาด place/latlng ผิด/;; ว่าง) จับได้ครบ 4 รายการ, debug ปิด reportVisible=false แต่ scene ที่ parse ผ่านยัง render 4 ฉากตามปกติ (fail-graceful ไม่กระทบ)\t คั่น ไม่มี key=value เลย) ยัง parse ได้ปกติ 100%, tag mode สคริปต์สมบูรณ์ไม่มีปัญหาแสดง "ไม่พบปัญหาในสคริปต์ล่าสุด" ถูกต้องtransport ดีฟอลต์ "none" เสมอมาตั้งแต่ก่อนพาท 18 แล้ว (ไม่ดีฟอลต์เป็นเครื่องบินกันโชว์พาหนะผิดยุคในฉากประวัติศาสตร์ก่อนมีเครื่องบิน)พาท 25 — Smooth Geographic Navigation + Full Scene Preload / Media Preparation
navigation: auto/smooth/direct (ดีฟอลต์ auto) — เปลี่ยนดีฟอลต์การขยับกล้องระหว่างฉากที่ไกลกันมากให้ "ซูมออก→เผยบริบทภูมิศาสตร์→เดินทาง→ซูมเข้า" แทนการตัดตรงแบบเดิม (เหตุผลชัดเจนตามที่กำหนด: กันภาพกระโดด/เวียนหัว) — ต่อยอด visualbridge= เดิม (พาท 20) ไม่ลบไม่เปลี่ยนชื่อ: visualbridge=on/off ที่ตั้งเองชัดเจนยัง priority สูงสุดเหมือนเดิมเป๊ะ, navigation=direct/smooth ที่ตั้งเองรองลงมา, ไม่ตั้งอะไรเลย (auto) ใช้ดีฟอลต์ใหม่ตาม travelmodetravelmode: local/regional/global — ไม่ใส่ = คำนวณจากระยะทางจริง (haversine) ระหว่างฉากก่อนหน้ากับฉากนี้: local <150กม. ไม่ต้องซูมออก, regional <1500กม. ซูมออกปานกลาง (zoom -0.5, floor 3, 500ms), global ไกลกว่านั้นซูมออกมากขึ้น (zoom -1, floor 2, 650ms) เผยบริบทกว้างขึ้นแต่ยังกันไม่ให้ซูมออกเกินจำเป็น — ทดสอบยืนยันครบ: ฉากใกล้กันไม่ bridge (เหมือนเดิม), ฉากไกลข้ามทวีป ไม่ต้องตั้ง visualbridge=on เอง ก็ bridge อัตโนมัติแล้ว (ดีฟอลต์ใหม่), navigation=direct และ visualbridge=off ยังบังคับตัดตรงได้เหมือนเดิมทุกจุด, cam=fly-to ไม่ผ่านระบบนี้เพราะมีการเดินทางโค้งในตัวอยู่แล้วPromise.all([preloadNarration, preloadImages])) แสดงข้อความสถานะ PREPARING → GENERATING VOICE → IMAGE PREPARATION → READY ระหว่างเตรียมgeophoto/callout/badge=flag:.../insert (ที่เป็นรูป) ทุกฉาก preload ด้วย Image() ผ่าน cache กันโหลดซ้ำถ้ารูปเดิมใช้หลายฉาก — สคริปต์ที่ไม่มีรูปเลย (ส่วนใหญ่) ผ่านทันที ไม่กระทบพฤติกรรมเดิม — ทดสอบแล้ว: ฉากมี geophoto แสดงสถานะ "1/1" ถูกต้อง เล่นจบไม่มี errorttsCache เดิมคีย์ด้วย เนื้อหาข้อความจริง (ไม่ใช่ index ฉาก) อยู่แล้วตั้งแต่ก่อนหน้านี้ — แก้สคริปต์แค่บางฉากจึงได้ cache hit ฉากที่ไม่เปลี่ยนโดยอัตโนมัติ ไม่ต้องแก้อะไรเพิ่ม — ทดสอบยืนยัน: เล่น 3 ฉากแล้วแก้เฉพาะฉาก 2 กด เล่นใหม่ cache โตขึ้นแค่ 1 รายการ (เฉพาะฉากที่แก้) ไม่ใช่ regenerate ทั้ง 3preloadNarration ไม่ถูกลบ/เปลี่ยนชื่อ แค่เพิ่มข้อความสถานะ, narrationLoop/fetchTts/playAudioClip ไม่ถูกแตะเลยพาท 26 — ลากไฟล์รูป/เสียงแบ่งฉากอัตโนมัติ (Drag & Drop Auto-Split)
scene2.jpg มาหลัง scene10.jpg) แล้วไฟล์ที่ 1 → ฉากที่ 1, ไฟล์ที่ 2 → ฉากที่ 2, ... ไฟล์เกินจำนวนฉากถูกข้าม (สถานะแจ้งจำนวนที่ไม่ได้ใช้), ต้องนำเข้าสคริปต์ก่อนถึงจะลากได้ — ลากไฟล์อัพโหลดผ่าน /api/upload เดิม (endpoint เดียวกับปุ่ม "เลือกไฟล์ภาพ" ที่มีอยู่แล้ว) ได้ URL ถาวรในโฟลเดอร์ assets/scene.geophoto.url ของฉากนั้น (ถ้าฉากมี geophoto= เดิมอยู่แล้วจะทับแค่ .url คง lat/lng/label เดิมไว้ ถ้ายังไม่มีจะสร้างใหม่ปักที่พิกัดของฉากเอง) — ใช้ระบบ render/preload รูปเดิมทั้งหมด (setGeoPhoto/collectSceneImageUrls) ไม่ต้องเพิ่มโค้ดแสดงผลใหม่เลยscene.overrideAudioUrl/overrideAudioDuration (ฟิลด์รันไทม์ใหม่ ไม่ใช่ tag) — ตอนเล่นจริง ฉากที่มีไฟล์เสียงอัพโหลดเองจะข้าม TTS ทั้งหมด (ทั้งใน preloadNarration และ narrationLoop) เล่นไฟล์เสียงที่ลากมาเป็นคลิปเดียวทั้งฉากแทน ซับไตเติลคาราโอเกะยังทำงานปกติ (ใช้ความยาวไฟล์เสียงจริงคำนวณจังหวะคำแทนที่จะประมาณจาก TTS) ความยาวไฟล์เสียงถูกวัดจริงมาตั้งเป็น scene.duration เหมือน TTS เดิมทุกจุด (จังหวะกล้อง/ระบบ pacing ทั้งหมดยังทำงานตามปกติ)Audio().duration มีการ์ดกัน edge case ที่เบราว์เซอร์รายงาน Infinity/NaN (ไฟล์เสียงเพี้ยน/สตรีม) ตกไปใช้ความยาวเดิมของฉากแทนเสมอ ไม่ปล่อยให้ค่าพังไปกระทบจังหวะฉากgeophoto=/TTS ปกติทุกจุดเหมือนเดิม 100%/api/tts เลย ฉากที่ 3 (ไม่มีไฟล์) ยังพากย์ TTS ตามปกติ ไม่มี error บน consoleพาท 27 — Smart Documentary Production Import (Core Loop v1)
image=01 / image=01,02 / video=01 / audio=01 — เลข sequence อ้างอิง asset ที่ลากเข้า Smart Import ไม่ใช่ url ตรงๆ (ต่างจาก geophoto= เดิมที่ต้องใส่ url เอง) เก็บใน scene.imageRefs/videoRefs/audioRefs (array, ดีฟอลต์ []) ไม่ใส่เลยสักฉาก = พฤติกรรมเดิม 100% — v1 ใช้แค่ตัวแรกของ list เป็นตัวจับคู่จริง (multi-ref เก็บไว้ในฉากแล้วแต่ยังไม่ทำ multi-asset sequence ต่อฉากตามข้อ 15 ในสเปก)asset_0001, asset_0002, ...) — ห้ามใช้ originalFilename เป็นตัวจับคู่หลักตามข้อ 6 ในสเปก (เก็บไว้แค่โชว์ผู้ใช้ดู) อัพโหลดผ่าน /api/upload เดิม (endpoint เดียวกับพาท 26) ได้ url ถาวรใน assets/ — รูปวัด width/height จริง, วิดีโอ/เสียงวัด duration จริง (มีการ์ดกัน Infinity/NaN เหมือนพาท 26)runAssetMatching, ข้อ 11 ในสเปก): ทำแค่ 2 tier ที่ deterministic 100% ไม่ต้องเดา (ตัด metadata/semantic/date matching ที่ต้องใช้ AI วิเคราะห์เนื้อหาไฟล์จริงออก เพราะ Zero Additional Cost Mode ห้ามเรียกบริการภายนอกเสียเงินและไม่มี local vision model ในโปรเจกต์): 1) Explicit Reference — image=/video=/audio= ตรงกับเลข sequence ของ asset ที่มีจริง ได้ confidence 100% (HIGH) ถ้าระบุเลขแต่หา asset ไม่เจอ = ขึ้นเป็น "ขาดไฟล์" ทันที (ไม่ตกไป fallback เงียบๆ ตามข้อ 21+39 ห้าม silent failure) 2) Sequence Matching — ฉากที่ไม่ได้ระบุเลขเลย ได้ asset ที่เหลือ (ยังไม่ถูกจับคู่) ไปเรียงตามลำดับไฟล์ที่อัพโหลด confidence 75% (MEDIUM) — asset หมดพูลเมื่อไหร่ฉากที่เหลือไม่ได้ asset เพิ่มเลย (กันไม่ให้ยัด video/image ทุกฉากจนกลายเป็น slideshow ตามข้อ 14 "Map-First" โดยไม่ต้องมี logic เลือกเองเลย เพราะพูล asset จริงมักน้อยกว่าจำนวนฉากอยู่แล้ว)พาท 28 — จัดหน้านำเข้าสคริปต์ใหม่เป็นแท็บ (UX cleanup)
<details> ซ้อนกัน 6 ชั้นในหน้าเดียว หาของยาก) — เปลี่ยนเป็นแถบแท็บด้านบน (สร้างฉากง่ายๆ / เขียนสคริปต์ / Smart Import / Excel / โปรเจกต์ / ตรวจสอบ / ตั้งค่า) กดปุ่มไหนแสดงเนื้อหาแท็บนั้น ซ่อนแท็บอื่น แทนการพับ/กางแบบเดิมbuilderDetails/smartImportDetails/ปุ่ม/ช่องกรอกทุกตัว), ปุ่ม "แก้ไข" บนการ์ดฉาก สลับไปแท็บ "สร้างฉากง่ายๆ" ให้อัตโนมัติเหมือนพฤติกรรมเดิมที่เคยกาง <details> ให้เองพาท 29 — MAP-FIRST Documentary Pattern (คู่มือ + ตัวอย่างโหลดได้ ไม่ใช่ระบบใหม่)
mapmode/insertmode/returnmap/density/Anti-Overediting) — ไม่ต้องสร้างระบบ AI ตัดสินใจ visual ใหม่ (ซึ่งจะต้องพึ่ง AI ภายนอกอยู่ดี) แค่ต้องเขียนสคริปต์ตามแพทเทิร์นที่ถูกต้อง จึงเพิ่มแค่คู่มือ + ปุ่มโหลดตัวอย่างสำเร็จรูป แทนการสร้างเอนจินใหม่cam=establishing/fly-to/push-in/orbit) = "MAP ONLY" ดีฟอลต์อยู่แล้ว → ฉากที่ต้องบอกแนวรบใช้ cam=battle-map + arrows= เฉพาะฉากนั้น → ต้องการภาพเล็กมุมจอไม่บังแผนที่ใช้ cam=insert-overlay + insertmode=picture-in-picture (ต่างจาก cut-to-insert ที่ตั้งใจให้เต็มจอ) → ต้องการตัดเต็มจอชั่วครู่แล้วกลับแผนที่ (EXPLAIN→SHOW→RETURN / MAP→MEDIA→MAP) แทรกฉาก cut-to-insert สั้นๆ 1 ฉากคั่นกลาง แล้วฉากแผนที่ที่ตามมาใส่ returnmap=on (กล้องสืบทอดตำแหน่งจากฉากแผนที่ก่อนหน้า ไม่กระโดดเริ่มใหม่ — ระบบเดิมตั้งแต่พาท 20) → ใส่ density=low/minimal ลดความเร็วกล้อง+ลด effect ตกแต่งจาก style preset อัตโนมัติ (เส้น/ลูกศร/insert ที่พิมพ์เองยังโชว์เสมอ เอ็นจินไม่มีสิทธิ์ลบสิ่งที่สั่งเองตรงๆ ตาม priority เดิม)insertMode:"picture-in-picture" ที่ฉาก insert, returnMap:true ที่ฉากสุดท้าย, density:"low" ตามที่ตั้ง) timeline chip แสดงป้ายกำกับถูกต้องครบ ไม่มี error บน consoleพาท 30 — เทสสคริปต์จริง 4 นาที (สงครามแปซิฟิก) + แก้จุดที่เจอ
arrows= เขียนผิดฟอร์แมตแล้วหายเงียบๆ: ตัวเองพิมพ์ arrows=ป้าย:สี:lat,lng;lat,lng (ใช้ ; คั่นจุดเริ่ม/จุดปลาย) ทั้งที่ฟอร์แมตจริงต้องใช้ : คั่น (; มีไว้คั่นระหว่างลูกศรหลายเส้น) — ลูกศรหายไปทั้งเส้นโดยไม่มี error ให้เห็นแม้เปิด Debug Mode เพราะ validateTagLine เดิมไม่เคยเช็ก arrows= เลย เพิ่มการตรวจฟอร์แมต arrows= ใน Debug Mode แล้ว (จำนวนจุด/สี/พิกัดผิดขึ้นเตือนบรรทัดที่ผิดชัดเจน)narrative=rising-action (ไม่มีในระบบ ค่าจริงคือ escalation/conflict/ฯลฯ) และ mood=hopeful (ค่าจริงคือ hope) — ทั้งคู่ตกไปใช้ auto-guess/default เงียบๆ โดยไม่มี warning เลยแม้เปิด Debug Mode ตรวจโค้ดพบว่า validateTagLine เดิม (ตั้งแต่พาท 24 ที่อ้างว่า "ตรวจสอบย้อนหลังทุกระบบ") เช็กแค่ 4 คีย์แรก (place/cam/script/latlng) ไม่เคยครอบคลุมคีย์ enum อีก 20+ ตัวที่เพิ่มมาทีหลัง (narrative/beat/mood/shot/cameraaction/motion/motioncurve/continuity/transition/mapmode/insertmode/inserttransition/evidence/pace/pacing/density/attention/emphasis/intensity/composition/navigation/travelmode) — เพิ่ม generic enum validator (ENUM_KEY_CHECKS) ตัวเดียวครอบคลุมครบทุกคีย์ enum เหล่านี้ ขึ้น warning บอกค่าที่รองรับจริงเมื่อพิมพ์ผิด ไม่กระทบ render ใดๆ (Debug Mode เท่านั้น)insertmode=picture-in-picture ไม่เล็กจริง — ขนาดเท่า overlay เป๊ะ (62-78% ของจอ): ขัดกับชื่อ "PiP" และหลักการ MAP-FIRST (พาท 29) ที่ต้องการภาพเล็กไม่บังแผนที่ ตรวจ CSS พบว่า .insertmode-picture-in-picture ใช้ selector ร่วมกับ .insertmode-overlay มาตั้งแต่แรก ไม่เคยมีขนาดแยกกันจริง — แก้ให้ picture-in-picture เล็กจริง (30% ของจอ แทน 62-78%) แยกขาดจาก overlay (คงขนาดกลาง 58-62% ไว้เหมือนเดิม) ทดสอบแล้วภาพเล็กมุมจอ เห็นแผนที่ข้างหลังชัดเจน ไม่ทับ cut-to-insert/insertmode=full ที่ยังเต็มจอปกติเหมือนเดิม (ทดสอบแยกยืนยันไม่มีการเปลี่ยนแปลง)sec= เป็นแค่ค่าประมาณเริ่มต้นก่อนพากย์เสียงจริง — เมื่อเล่นจริงแล้ว engine จะปรับ ตามความยาวเสียงพากย์จริงเสมอ (ไม่ใช่ตามตัวเลข ที่พิมพ์) ดังนั้นความยาววิดีโอรวมจริงขึ้นกับความยาวบทพากย์ที่เขียน ไม่ใช่ผลรวม ที่ตั้งไว้ — สคริปต์ทดสอบนี้ตั้ง รวม 236 วินาทีแต่เวลาจริงสั้น/ยาวกว่านั้นได้ขึ้นกับความยาวบทพากย์แต่ละฉาก (มีอยู่แล้วตั้งแต่การออกแบบเดิม ไม่ต้องแก้ไข แค่บันทึกไว้ให้เข้าใจตรงกัน)พาท 31 — ดึงภาพประกอบอัตโนมัติทุกฉาก (ขยายจากเดิมที่มีแค่ในแผงสร้างฉากทีละฉาก)
/api/imagesearch (Wikimedia Commons ฟรี) และฟังก์ชัน searchAutoImage() อยู่แล้วตั้งแต่ก่อนหน้านี้ แต่ผูกไว้แค่ใน "สร้างฉากง่ายๆ" (checkbox "ดึงภาพประกอบอัตโนมัติ" ตอนเพิ่มฉากทีละฉากเท่านั้น) — สคริปต์ที่พิมพ์/วางตรงๆ (แท็บเขียนสคริปต์) หรือมาจาก Smart Import ไม่มีทางได้ภาพอัตโนมัติเลยgeophoto=, ไม่ได้จับคู่จาก Smart Import) แล้วค้น Wikimedia Commons ตามชื่อ place= ให้อัตโนมัติ ก่อนเข้าสู่ Full Scene Preload เดิม (พาท 25)cut-to-insert/insert-overlay เสมอ (ต้องใส่ insert= เองเท่านั้น ตามกฎ "ห้ามสร้าง evidence/insert เองอัตโนมัติ" ที่ตกลงไว้ตั้งแต่พาท 21) — ดึงเฉพาะ geophoto ของฉากแผนที่ปกติเท่านั้นplace=) กันค้นซ้ำถ้าหลายฉากใช้สถานที่เดียวกัน (เช่นกลับมาที่เมืองเดิมหลายฉาก) — หาไม่เจอ = ไม่มีรูปเหมือนเดิม ไม่ error ไม่กระทบการเล่นต่อgeophoto=/Smart Import อยู่แล้ว (ข้ามไปเลยไม่ค้นซ้ำ)พาท 32 — เลือกดึงภาพอัตโนมัติ/ใส่เองได้เป็นรายฉาก
autoimage=on / autoimage=off ตั้งได้รายฉาก priority สูงสุดทับ toggle รวมเสมอ (ตรงตามหลัก explicit ผู้ใช้ > ตัวเลือกรวม > ดีฟอลต์ ที่ใช้สม่ำเสมอทั้งระบบ):
autoimage=off — ฉากนี้จะไม่ถูกดึงภาพอัตโนมัติเด็ดขาด แม้เปิด toggle รวมไว้ก็ตาม (ไว้ใส่รูปเองแน่ๆ ไม่ให้ระบบไปแทนที่)autoimage=on — ฉากนี้ถูกดึงภาพอัตโนมัติเสมอ แม้ปิด toggle รวมไว้ก็ตาม (อยากได้ภาพประกอบเฉพาะบางฉากโดยไม่ต้องเปิดทั้งสคริปต์)geophoto=/Smart Import จับคู่ไว้แล้ว หรือเป็นฉาก cut-to-insert/insert-overlay ยังถูกข้ามเสมอไม่ว่า autoimage= จะตั้งอะไรไว้ (ตรรกะเดิมพาท 31 ไม่เปลี่ยน — ป้องกันการเขียนทับรูปที่ผู้ใช้ใส่เองไว้แล้ว)autoimage=on → ฉากนั้นได้ภาพ ฉากอื่นที่ไม่ตั้งอะไรยังว่างเหมือนเดิม (2) toggle รวมเปิด + ฉากหนึ่งใส่ autoimage=off → ฉากนั้นว่างต่อ ฉากอื่นได้ภาพตามปกติ ไม่มี error บน consoleพาท 33 — ดึงภาพประกอบให้ตรงยุคเหตุการณ์ (ไม่ใช่แค่ตรงสถานที่)
year=1941 (ปี ค.ศ.) ตั้งได้รายฉาก — ใช้ระบุปีเหตุการณ์ตรงๆ ให้ค้นภาพแม่นขึ้นyear= เองก็เดาให้อัตโนมัติจากบทพากย์: หาเลขปี ค.ศ. ตรงๆ ในสคริปต์ (เช่น "1941") หรือ พ.ศ. (เช่น "พ.ศ. 2484" แปลงเป็น ค.ศ. อัตโนมัติ = 1941 - 543) — เจอปีก็เอาไปต่อท้ายชื่อสถานที่ตอนค้น Wikimedia Commons ให้เอง ไม่เจอปีเลย = ค้นแค่ชื่อสถานที่เหมือนเดิม (ไม่กระทบพฤติกรรมเดิมถ้าสคริปต์ไม่มีตัวเลขปีอยู่แล้ว)year= เองเสมอสำหรับฉากประวัติศาสตร์ (ยังไม่การันตีตรงยุค 100% เพราะสถานที่ภาษาไทยอาจหาไม่เจอเลยก็ได้ แต่แม่นขึ้นชัดเจนกว่าไม่ใส่)year= แต่สคริปต์มีคำว่า "1941" → เดาปีถูกต้อง ได้รูปเหตุการณ์จริงปี 1941 อัตโนมัติ, ฉาก "ฮิโรชิม่า" ใส่ year=1945 ตรงๆ → ค้นไม่เจอ (ชื่อไทย+ปี) fallback กลับไปได้รูปสถานที่ปัจจุบันแทน (ตัวอย่างข้อจำกัดที่พบจริง) ไม่มี error บน console ทั้งสองกรณีพาท 34 — ปรับขนาดการ์ดภาพประกอบปักพิกัดได้
photosize=120 (px) ปรับขนาดการ์ด geophoto=/รูปจาก Smart Import/วิดีโอปักพิกัด (เป็นสี่เหลี่ยมจัตุรัส) เฉพาะฉากนั้น ไม่ใส่ = ดีฟอลต์ 84px เดิมเป๊ะ ไม่กระทบสคริปต์เก่า.geo-photo-card (รูปนิ่ง) และ .geo-video-card (วิดีโอจาก Smart Import พาท 27) เพราะเป็นการ์ด "ภาพประกอบปักจุด" ชุดเดียวกัน ตั้งค่าเดียวคุมทั้งคู่photosize=180 การ์ดใหญ่ขึ้นชัดเจน ฉากที่ไม่ใส่ยังคง 84px เดิม ไม่มี error บน consoleพาท 35 — Auto Director: เดา cam อัตโนมัติ + Visual Rhythm diagnostic + Visual Makeup label
cam= เอง (rule-based ล้วนๆ ไม่มี AI ภายนอก ไม่สร้าง arrows/highlight/insert ให้เองเด็ดขาด ตามกฎ Global Compatibility Rules พาท 24 ที่ห้ามสร้างหลักฐาน/ข้อมูลเอง)cam= ไม่ใส่ก็ได้แล้ว (เดิมเป็นคีย์บังคับ 3 อย่าง place+cam+script — ไม่ใส่ cam ฉากทั้งบรรทัดถูกข้ามเงียบๆ ทั้งฉาก) ตอนนี้บังคับแค่ place+script — ไม่ใส่ cam หรือสะกดผิด ระบบเดามุมกล้องให้จากคำในบทพากย์ (inferCam): เจอคำเกี่ยวกับรบ/โจมตี/รุกราน/สงคราม/ล้อม → battle-map, เจอคำเกี่ยวกับเดินทาง/เคลื่อนทัพ/มุ่งหน้า → fly-to, เดาไม่ออก → establishing (ดีฟอลต์กลางสุดเดิม) — cam= ที่พิมพ์เองถูกต้องยังชนะเสมอ ไม่กระทบสคริปต์เก่าที่ระบุ cam ครบทุกฉากเลยแม้แต่นิดเดียว (ทดสอบยืนยัน camSource:"user" ทุกฉากเหมือนเดิม)camSource ("user"/"auto") เก็บไว้ในทุกฉากเหมือน narrative/beat/mood — โชว์ในการ์ดพรีวิวเป็น cam:xxx (auto, Director) เฉพาะตอนเดาเท่านั้น โปร่งใสว่าระบบตัดสินใจอะไรไปบ้างsceneVisualMakeup) สรุปว่าฉากนั้นมีองค์ประกอบภาพจริงอะไรบ้าง (MAP / MAP + IMAGE / MAP + VIDEO / MAP + GRAPHIC (ลูกศร) / MAP + INSERT เล็ก ฯลฯ) — เป็นแค่ label สรุปให้ดู ไม่ได้ไปเปลี่ยนการ render ใดๆ ตรงกับคำศัพท์ "Final Visual Decision" ในสเปกข้อ 31checkVisualRhythm) เตือนถ้าฉากติดกันตั้งแต่ 4 ฉากขึ้นไปใช้ cam เดิมซ้ำ หรือมีรูป/วิดีโอประกอบทุกฉากติดกัน (เสี่ยงกลายเป็น slideshow ตามสเปกข้อ 16) — เตือนเฉยๆ ไม่ได้แก้ฉากให้เอง, ไม่เตือนรันของฉาก "ไม่มีรูป" ติดกัน (นั่นคือ MAP-ONLY ที่ถูกต้องตามหลัก MAP-FIRST อยู่แล้ว ไม่ใช่ปัญหา)arrows=/highlight=/insert= จากบทพากย์เด็ดขาด เพราะต้องแต่งพิกัด/หลักฐานขึ้นเองซึ่งขัดกับกฎ "ห้ามสร้างข้อมูล/หลักฐานเอง" ที่ยึดมาตั้งแต่พาท 24 — Auto Director เดาได้แค่ "มุมกล้อง" (การตีความ ไม่ใช่ข้อมูลใหม่) ส่วนที่เหลือ (arrows/highlight/insert) ยังต้องให้ผู้ใช้ระบุพิกัด/เนื้อหาจริงเองเสมอxyz-invalid fallback ไป inferCam แทน establishing เฉยๆ, rhythm diagnostic เตือน 5 ฉากติดกัน cam เดิมถูกต้อง และ 4 ฉากติดกันมีรูปถูกต้อง (ฉากไม่มีรูปคั่นแล้วไม่เตือนย้อนหลัง), สคริปต์เก่าที่ระบุ cam ครบ (สคริปต์สงครามแปซิฟิกพาท 30) camSource เป็น "user" ทุกฉากเหมือนเดิม ไม่มี error บน consoleพาท 36 — เครื่องมือค้นข้อมูลจริงจาก Wikidata (manual, ไม่ auto-apply)
latlng=... | year=...) ไปวางในสคริปต์เอง (รูปแบบเดียวกับปุ่ม "คัดลอกไฟล์" ที่มีอยู่แล้วในแผงอัพโหลดภาพ)/api/wikidata?q=... proxy wbsearchentities+wbgetentities ของ Wikidata (ค้นภาษาไทยก่อน ถ้าไม่เจอลองอังกฤษต่อ) แคชผลลัพธ์ตาม query กันเรียกซ้ำพาท 37 — เพิ่ม 2 ใน 4 API ฟรีที่เสนอไว้ (อีก 2 ทดสอบแล้วไม่พร้อมใช้จริง)
/api/archivesearch (ค้นชื่อ/คำอธิบายคลิป mediatype:movies) + /api/archivefile (ดึงรายชื่อไฟล์ของรายการที่เลือก เลือกไฟล์ .mp4 ที่ไม่ใช่ "Preservation Master" (มักใหญ่เป็น GB) และเล็กสุดที่พอเล่นได้) — UI ใหม่ในแท็บ "สร้างฉากง่ายๆ" ค้นแล้วโชว์ชื่อ+คำอธิบายให้เช็กก่อน กด "ดึงลิงก์เล่นได้" แล้วคัดลอก tag videourl= สำเร็จรูป ทดสอบค้น "pearl harbor newsreel 1941" ได้คลิปข่าวจริงปี 1942 (National Archives) ขนาด 61.8MB เล่นได้จริง/api/audiosearch (endpoint เดิมจากพาท 26 ขยายแค่เติม filetype:audio ในคำค้น ไฟล์เสียงอยู่ namespace เดียวกับรูปอยู่แล้ว) ทดสอบค้น "air raid siren" ได้ไฟล์เสียงไซเรนจริงหลายไฟล์ คัดลอก tag audiourl= ได้ทันทีvideourl=<url> (ปักการ์ดวิดีโอที่ฉากตรงๆ เหมือน geophoto= ไม่ผ่าน asset manifest ของ Smart Import) และ audiourl=<url> (ข้าม TTS ใช้ไฟล์นี้พากย์แทนทั้งฉาก duration ที่ไม่รู้ตอน parse จะถูก probe จริงให้อัตโนมัติก่อนเข้า narrationLoop เสมอ ผ่านฟังก์ชันใหม่ probeExplicitAudioUrls)highlight= เดิมได้ ไม่ implementlibretranslate.com) เรียก /translate แล้วได้ error ทันที ("server could not understand") บ่งชี้ว่า public instance ตอนนี้ต้องใช้ API key/free-tier สมัครเองแล้ว ไม่ใช่ keyless แบบที่ตั้งใจไว้ตอนแรก ไม่ implementพาท 38 — เทสสคริปต์ยาวจริง (สงครามรัสเซีย-ยูเครน 31 ฉาก) + แก้คอขวดที่เจอ 4 จุด
examples/russia-ukraine-10min.txtcallout=/badge=/geophoto=/warmorph=/focuspoint=/draw= เขียนผิดฟอร์แมตแล้วหายเงียบๆ (ปัญหาคลาสเดียวกับ arrows= ที่แก้ไปในพาท 30 แต่คีย์อื่นยังไม่ได้ตรวจ) — เพิ่มการตรวจฟอร์แมตด้วย parser จริงของแต่ละคีย์ใน Debug Mode ทันทีที่เพิ่ม validator ตัวนี้ มันจับ draw= ที่เขียนผิดในสคริปต์เทสของตัวเองได้ทันที (ใช้ : คั่นจุดแทน ;)/api/tts ล้มเหลว ~ครึ่งหนึ่งของคำขอตอนเตรียมเสียงรัวๆ (คือ error 500 ที่เห็นมาตลอดทั้งโปรเจกต์และเคยสรุปว่าเป็น noise ไม่เกี่ยวกับโค้ด — ที่จริงคือ edge-tts ตัดการเชื่อมต่อเมื่อยิงถี่) ทำให้ฉากนั้นไม่มีเสียงในแคช แล้วไปค้างกลางคลิปตอนเล่นจริง — เพิ่ม retry 3 ครั้งพร้อมหน่วงเพิ่มขึ้นใน synthesize() ผลคือ 500 หายเกลี้ยง เหลือ 200 ล้วน.tts-cache/, คีย์ sha1 ของ voice+rate+pitch+ข้อความ) — คนทำคอนเทนต์กดเล่นซ้ำหลายรอบระหว่างแก้สคริปต์ ประโยคที่ไม่ได้แก้ไม่ต้องรอ edge-tts ใหม่ทุกครั้ง (แคชในเบราว์เซอร์เดิมหายทุกครั้งที่รีเฟรช)photosize= ได้เหมือนเดิมต้องรัน server.py (ไม่ใช่ python -m http.server เฉยๆ) เพราะต้องมี endpoint /api/tts สำหรับเสียงพากย์:
python -m pip install -r requirements.txt
python server.py 5173
แล้วเปิด http://localhost:5173 กด "นำเข้าสคริปต์" → "แปลงเป็นฉาก" (มีข้อมูลตัวอย่างในช่องอยู่แล้ว) แล้วกดเล่น
ถ้าเปิดแค่ index.html ตรงๆ หรือรันด้วย http.server ธรรมดา แผนที่/หมุด/สคริปต์ยังใช้ได้ปกติ แต่กดเล่นแล้วจะไม่มีเสียง (fetch /api/tts จะพัง)
ต้องมี server.py รันอยู่ที่ localhost:5173 ก่อน (คนละหน้าต่างเทอร์มินัล) แล้วรัน:
python -m playwright install chromium # ครั้งแรกครั้งเดียว โหลด Chromium ~150MB
python render.py my-script.txt --aspect 916 --out my-video.mp4
my-script.txt คือไฟล์ข้อความสคริปต์ซีน ฟอร์แมตเดียวกับที่วางในกล่อง "นำเข้าสคริปต์" บนเว็บ (เอาไฟล์ที่บันทึกโปรเจกต์ไว้มาแกะ script ออกมาเป็นไฟล์ .txt ก็ได้ หรือพิมพ์ตรงๆ)--aspect 916 แนวตั้ง หรือ 169 แนวนอน--voice th-TH-NiwatNeural เปลี่ยนเป็นเสียงชาย (ดีฟอลต์เสียงหญิง Premwadee), --rate +20% / --pitch -10Hz ปรับความเร็ว/โทนเสียง--gap 1.5 พักเงียบ 1.5 วินาทีระหว่างฉาก--bgm เพลง.mp3 --bgm-volume 0.25 ผสมเพลงพื้นหลัง (วนซ้ำอัตโนมัติจนจบคลิป)--subpos top --subcolor ff3333 --subsize 24 --subweight 800 ปรับสไตล์ซับไตเติลหลักให้ตรงกับที่ตั้งไว้บนเว็บ (ค่าเหล่านี้ไม่ได้ดึงจากเว็บอัตโนมัติ ต้องพิมพ์ค่าที่ต้องการเองตอนรันคำสั่ง)แบบ tag (แนะนำ — ยืดหยุ่น เรียงคีย์ไหนก่อนก็ได้ ไม่ใส่คีย์ไหนก็ default ให้):
place=สุโขทัย | latlng=17.0175,99.7016 | cam=fly-to | script=... | sec=6 | icon=castle | effect=storm | tilt=45 | bearing=200 | highlight=province | insert=...
ใส่แค่คีย์ที่จำเป็นก็พอ เช่น place=... | cam=orbit | script=... | tilt=50
แบบ column เดิม (เรียงตำแหน่งตายตัว ใช้กับไฟล์ Excel/render.py ก็ยังได้):
สถานที่ | lat,lng | แผนกล้อง | สคริปต์เสียง | วินาที | ไอคอน | เอฟเฟกต์ | insert
ใช้ทั้งสองแบบปนกันในไฟล์เดียวได้ (เช็คต่อแถว) — ระบบดูว่าแถวนั้นมี key=value ไหม ถ้ามีใช้ tag-mode ถ้าไม่มีถอยไปใช้ column-mode
city / castle / ship / battle / flag / mountain / camp (ไม่ใส่ = 📍)storm / fire / battle / nonetilt (0-60°) / bearing (0-360°) — ปรับมุมเอียง/ทิศกล้องเฉพาะฉากนั้น ใช้ได้เฉพาะ tag-mode (column-mode ไม่มีช่องให้ใส่)highlight: country / province / place / none — ไฮไลต์เขตแดนจริงจาก OpenStreetMap (Nominatim) ตรงจากพิกัดฉาก พร้อมป้ายชื่อเขต + เส้นขอบลากทีละนิดแบบปากกาวาด (border trace) ใช้ได้เฉพาะ tag-mode เท่านั้น ต้องมีอินเทอร์เน็ตและรัน server.py (ไม่ใช่ http.server เฉยๆ) เพราะเรียก /api/boundary ผ่านเซิร์ฟเวอร์landfill=flag — เติมพื้นที่ไฮไลต์เป็นลายธงชาติจริงแทนสีทึบ (ดึงจาก /api/flag ต้องมี highlight ด้วย)cam=battle-map คู่กับ arrows=ฝ่าย1:RRGGBB:lat,lng:lat,lng;ฝ่าย2:... — ลูกศรเดินทัพหลายเส้นพร้อมกัน แยกสีตามฝ่าย มุมกล้องสไตล์ RTS (เอียงคงที่ ไม่หมุน)transport: plane / car / ship / train / walk — ไอคอนพาหนะวิ่งไปตามเส้นทางตอน cam=fly-to (ดีฟอลต์ plane)shade: on หรือตัวเลข (รัศมี px) — เอฟเฟกต์สปอตไลต์ มืดรอบข้าง เหลือจุดสนใจสว่างhide: รายชื่อคั่นด้วย , จาก pin,boundary,arrow,timeline,topbar,brand — ซ่อนเฉพาะฉากนั้น เช่นอยากได้ภาพแผนที่โล่งๆ ไม่มี UIwarmorph=RRGGBB:lat1,lng1:lat2,lng2 — ต้องมี highlight ด้วย พื้นที่ฝั่งขวาของเส้น (ยืนที่จุด1 หันไปจุด2) เปลี่ยนสีเป็น RRGGBB ทีละนิดreveal=fade/wipe/split/circular/diamond — ต้องมี highlight ด้วย ลักษณะการเผยพื้นที่ (ดีฟอลต์ fade)trace=one/two/tworeverse/four — ต้องมี highlight ด้วย จำนวนจุดเริ่มลากเส้นขอบเขตพร้อมกัน (ดีฟอลต์ one)mainland=on — ต้องมี highlight ด้วย ตัดเกาะ/ดินแดนไกลออกไปทิ้งตอนคำนวณกรอบกล้องlabelfont / labelsize / labelweight — ปรับฟอนต์/ขนาด(px)/น้ำหนัก (เช่น 400,600,900) ของป้ายชื่อหมุดเฉพาะฉากนั้นcut-to-insert / insert-overlay — อัพโหลดภาพของตัวเองได้จากปุ่ม "เลือกไฟล์ภาพ" ในแผงนำเข้าสคริปต์ (เก็บไว้ที่โฟลเดอร์ assets/ ของเครื่องที่รัน server.py) กดที่ path ในลิสต์เพื่อคัดลอกมาวางในคอลัมน์นี้;; เพื่อแบ่งซับไตเติลหลายช่วงในฉากเดียว (ห้ามใช้ | เพราะชนกับตัวคั่นคอลัมน์/tag)<select id="voiceSelect"> เอง (edge-tts รองรับเสียงอื่นอีกเยอะ)render.py ยังไม่ fast-render (ใช้เวลาเท่าความยาววิดีโอจริง) — ถ้าจะเร็วกว่านี้ต้องขับกล้อง/แอนิเมชันแบบ deterministic frame-stepping แทนการเล่นสดhighlight= เรียก Nominatim สาธารณะ (ฟรีแต่จำกัด ~1 req/sec ต่อเครื่อง, cache ไว้แล้วแต่ยังไม่มี retry ถ้า Nominatim ล่ม/บล็อก) — และตั้งแต่พาท 12 ที่ไล่ยิงหลาย zoom ต่อการค้นหนึ่งครั้ง พื้นที่ที่หาระดับตรงไม่เจอง่ายๆจะยิ่งช้าขึ้น (ครั้งละหลายวินาที) แต่ผลถูกแคชไว้แล้วเรียกซ้ำจะเร็วทันทีhighlight= ของตัวเอง — place ครอบคลุมถึงระดับเขต/อำเภอ/เมืองเท่านั้น และหมู่บ้านส่วนใหญ่ใน OSM เก็บเป็นจุดไม่ใช่รูปพื้นที่ จึงไฮไลต์เป็นขอบเขตไม่ได้อยู่ดีmap.setStyle() ของ MapLibre ต้องลงทะเบียน source/layer ที่เราสร้างเองทั้งหมดใหม่ทุกครั้งที่สลับสไตล์ (เขียนโค้ดส่วน map.on("load", ...) ใหม่ให้เรียกซ้ำได้) — งานถัดไปถ้าต้องการscenes[] ผ่านฟิลด์ที่ engine เดิมรู้จักอยู่แล้วเป๊ะ — รูป→scene.geophoto (ทับแค่ .url คง lat/lng/label เดิมถ้ามี, เหมือนพาท 26), เสียง→scene.overrideAudioUrl/overrideAudioDuration (ข้าม TTS เหมือนพาท 26), วิดีโอ→scene.videoAsset ใหม่ (ดูข้อถัดไป) แล้วเรียก renderTimeline()/goToScene(0) ปลายทางเดียวกับ parse ปกติทุกจุด — เล่นได้ทันทีเหมือนนำเข้าสคริปต์แบบเดิมmarkerGeoVideo/geoVideoWrapEl คู่ขนานกับ markerGeoPhoto เดิม (offset คนละตำแหน่งไม่ทับกัน โชว์พร้อมกันได้ถ้าฉากมีทั้งรูปและวิดีโอ) เล่นวนแบบ muted อัตโนมัติเมื่อฉากมี scene.videoAsset — ฟังก์ชันเดิม (setGeoPhoto/goToScene/preload/cache) ไม่ถูกแตะเลยสักบรรทัด แค่เพิ่ม setGeoVideo(scene) เรียกคู่กันในจุดเดียวzeroCostMode — v1 นี้ไม่มีจุดไหนเรียก paid API เลยอยู่แล้ว (matching engine ทำงานจาก id/sequence/reference ล้วนๆ) ตัวแปรนี้เตรียมไว้เป็น guard สำหรับฟีเจอร์เสริมในอนาคตที่อาจต้องพึ่งบริการภายนอกimage=/video=/audio=/geophoto= เลยสักคีย์ — parse ได้ปกติ imageRefs:[] ทุกฉาก ไม่กระทบอะไรเลย, สคริปต์เก่าที่ใช้ geophoto=/พาท 26 drop-zone เดิมก็ยังทำงานคู่ขนานได้ตามปกติ (คนละกลไก ไม่ชนกัน)image=01|video=01|audio=01 ครบ, ฉาก 2 ไม่ระบุเลย, ฉาก 3 ระบุ image=99 ที่ไม่มีจริง) + bulk ingest 2 รูป/1 วิดีโอ/1 เสียง → กดวิเคราะห์ได้ผล: ฉาก 1 ทั้ง 3 ช่อง HIGH 100% (explicit), ฉาก 2 image ได้ MEDIUM 75% (sequence fallback) video/audio ว่างเพราะพูลหมด (ไม่ flood), ฉาก 3 ขึ้น "ขาดไฟล์" ถูกต้อง — override ผ่าน dropdown ทดสอบแล้ว (asset ถูกใช้ซ้ำได้), กดอนุมัติแล้ว scenes[] ได้ geophoto/videoAsset/overrideAudioUrl ตรงตามที่ review ไว้ทุกจุด, เล่นจริงยืนยัน 2 ฉากที่มีไฟล์เสียงข้าม TTS จริง ฉากที่ไม่มีไฟล์ยังพากย์ TTS ปกติ, วิดีโอ marker render ขึ้นจริงคู่กับ geophoto card ไม่ทับกัน, เล่นจบครบ 3 ฉากไม่มี error บน console (มีแค่ noise 500 เดิมที่ไม่เกี่ยวกับโค้ดนี้), ทดสอบสคริปต์เก่าไม่มี asset key เลยแยกต่างหาก ยืนยัน parse ปกติ 100%webkitdirectory), 3-panel workspace เต็มรูปแบบแยกหน้า, Engine Command Library (เอกสาร command ทั้งหมดในตัว), Visual Role tagging (EVIDENCE/CONTEXT/ฯลฯ), multi-asset sequence ต่อฉาก (map→image→video ในฉากเดียวตามลำดับเวลาย่อย), Import Session save/load แยกจากโปรเจกต์หลัก (ตอนนี้ asset manifest ถูกเก็บรวมในไฟล์โปรเจกต์ .json เดิมแทน — โหลดโปรเจกต์แล้ว asset ก็กลับมาเหมือนเดิม), full 25 test-case matrix ตามสเปก (ทดสอบแค่ smoke test ตัวแทนที่ครอบคลุม explicit/fallback/missing/override/reuse/backward-compat)scene.durationsec=sec=sec=sec= รวม 668 วินาที แต่ความยาวจริงที่ได้คือ 340 วินาที เพราะความยาวฉากจริงมาจากความยาวเสียงพากย์เสมอ (พาท 30) — วัดได้ว่าเสียงไทยพูดที่ ~12.9 ตัวอักษรต่อวินาที ดังนั้นถ้าอยากได้คลิป 10 นาทีจริงต้องเขียนบทพากย์ราว 7,700 ตัวอักษร (สคริปต์นี้มี 4,377 ตัวอักษร)|