diff --git a/.gitignore b/.gitignore index 82fc468..c2237ed 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,10 @@ dist-ssr /electron/native/screencapturekit/.build/ /electron/native/screencapturekit/.swiftpm/ /electron/native/bin/ +/tools/ocr/build/ +/tools/ocr/dist/ +/tools/ocr/models/**/.gitattributes +/tools/ocr/models/**/README.md # Native macOS generated files DerivedData/ @@ -40,6 +44,8 @@ xcuserdata/ release/** *.kiro/ .claude/ +__pycache__/ +*.py[cod] # npx electron-builder --mac --win # Playwright @@ -63,3 +69,8 @@ result-* #others **/*.import + +# Local agent/tooling state +/.agent/ +/.serena/ +/.venv-ocr-build/ diff --git a/docs/engineering/auto-user-guide-roadmap.md b/docs/engineering/auto-user-guide-roadmap.md new file mode 100644 index 0000000..cb8469b --- /dev/null +++ b/docs/engineering/auto-user-guide-roadmap.md @@ -0,0 +1,935 @@ +# Quy trình triển khai Auto User Guide Generation + +Mục tiêu của tính năng này là biến OpenScreen từ công cụ quay màn hình thành công cụ tự tạo tài liệu hướng dẫn sử dụng phần mềm. Người dùng bật Guide Mode, quay thao tác như bình thường, hệ thống ghi lại thời điểm click hoặc hotkey, trích ảnh từ video sau khi quay xong, chạy OCR local để đọc chữ trên giao diện, sau đó dùng AI tạo bản nháp hướng dẫn từng bước. + +Tài liệu này được viết để có thể bắt đầu coding ngay: có kiến trúc, schema, file cần thêm/sửa, thứ tự task, tiêu chí test và định nghĩa MVP. + +## Trạng Thái MVP Hiện Tại + +- Đã có Guide Mode trong HUD, ghi click/marker vào `.guide.json`. +- Đã có GuidePanel trong editor để chạy: prepare events, capture snapshots, OCR, generate draft, export Markdown/HTML. +- Đã có local deterministic draft để test không cần DeepSeek key. +- DeepSeek được gọi khi chọn provider `DeepSeek` và có `DEEPSEEK_API_KEY`. +- OCR local mặc định gọi `OPENSCREEN_GUIDE_OCR_URL` hoặc `http://127.0.0.1:8866/ocr`. +- Verification hiện tại: targeted guide tests pass, `npm test` pass, `npm run build-vite` pass, `npm run i18n:check` pass. + +## Mục Tiêu Sản Phẩm + +Flow người dùng: + +1. Bật Guide Mode. +2. Quay màn hình phần mềm cần hướng dẫn. +3. Trong lúc quay, hệ thống tự ghi timestamp các click chuột. +4. Người dùng có thể bấm một hotkey/nút marker nếu muốn đánh dấu bước thủ công. +5. Sau khi dừng quay, hệ thống trích ảnh màn hình từ video tại các timestamp đó. +6. OCR local đọc text trên ảnh giao diện. +7. Hệ thống map vị trí click tới text/control gần nhất. +8. AI Agent tạo tài liệu dạng từng bước. +9. Người dùng review, sửa nội dung, export Markdown/HTML. + +Ví dụ output: + +```md +# Hướng dẫn xuất báo cáo + +## Bước 1: Mở phần cài đặt + +Nhấn nút **Settings** ở thanh điều hướng bên trái. + +## Bước 2: Chọn Export + +Trong màn hình Settings, chọn **Export report**. +``` + +## Phạm Vi MVP + +MVP cần làm: + +- Bật/tắt Guide Mode trước khi quay. +- Tận dụng recorder hiện tại, không viết recorder mới. +- Tận dụng `.cursor.json` hiện tại để lấy click timestamp. +- Thêm marker bằng hotkey hoặc nút trên HUD. +- Tạo sidecar `.guide.json` riêng cho guide. +- Trích screenshot sau khi quay xong, từ video đã lưu. +- OCR local bằng PaddleOCR service. +- Tạo step candidate từ click position + OCR blocks. +- Gọi DeepSeek bằng text metadata, không gửi ảnh mặc định. +- Có panel review trong editor. +- Export Markdown và HTML. + +Không làm trong MVP: + +- Không chụp screenshot realtime trong lúc quay nếu chưa có benchmark cần thiết. +- Không gửi raw screenshot lên cloud AI mặc định. +- Không sửa schema `.cursor.json` nếu không bắt buộc. +- Không build full UI automation engine. +- Không làm PDF/DOCX ngay. +- Không bundle OCR runtime vào app packaged ngay. + +## Code Hiện Có Cần Tận Dụng + +Các điểm đã có trong codebase: + +- Recording orchestration: `src/hooks/useScreenRecorder.ts` +- Launch/HUD UI: `src/components/launch/LaunchWindow.tsx` +- Source selection: `src/components/launch/SourceSelector.tsx` +- Editor chính: `src/components/video-editor/VideoEditor.tsx` +- Project/session persistence: `src/components/video-editor/projectPersistence.ts` +- Cursor contracts: `src/native/contracts.ts` +- Hook đọc cursor data: `src/native/hooks/useCursorRecordingData.ts` +- IPC main handlers: `electron/ipc/handlers.ts` +- Native bridge: `electron/ipc/nativeBridge.ts` +- Cursor service: `electron/native-bridge/services/cursorService.ts` +- Windows cursor recording: `electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts` +- macOS cursor recording: `electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts` +- Frame/export primitives: `src/lib/exporter/frameRenderer.ts` + +Nhận định kỹ thuật: + +- Windows/macOS native cursor recording đã có dữ liệu click. +- Cursor sample hiện có thể có `interactionType: "click" | "mouseup" | "move"`. +- Editor hiện đã dùng click timestamp để render hiệu ứng click. +- Vì schema cursor đang được nhiều nơi dùng, MVP nên tạo `.guide.json` riêng thay vì mở rộng `.cursor.json`. + +## Kiến Trúc Tổng Thể + +```mermaid +flowchart TD + A["User bật Guide Mode"] --> B["Quay video bằng recorder hiện tại"] + B --> C["Cursor recorder ghi click timestamp"] + B --> D["Hotkey/HUD marker ghi manual event"] + C --> E["Dừng quay"] + D --> E + E --> F["Guide assembler tạo .guide.json"] + F --> G["Snapshot extractor seek video và xuất PNG"] + G --> H["PaddleOCR local đọc text + bounding boxes"] + H --> I["Target mapper map click tới OCR text/control"] + I --> J["DeepSeek/local LLM viết draft guide"] + J --> K["GuidePanel cho user review/sửa"] + K --> L["Export Markdown/HTML"] +``` + +Quyết định chính: + +- Realtime recording chỉ ghi event/timestamp, không xử lý OCR/AI. +- Screenshot được trích từ video sau khi quay, tránh ảnh hưởng performance recorder. +- OCR chạy local-first. +- DeepSeek chỉ nhận text metadata trừ khi user opt-in gửi ảnh. +- Guide data nằm cạnh recording artifact. + +## File Cần Thêm + +```text +src/guide/ + contracts.ts + eventBuilder.ts + targetMapper.ts + promptBuilder.ts + generatedGuideSchema.ts + snapshot/ + extractGuideSnapshots.ts + export/ + markdownExporter.ts + htmlExporter.ts + __tests__/ + eventBuilder.test.ts + targetMapper.test.ts + promptBuilder.test.ts + markdownExporter.test.ts + +src/components/video-editor/guide/ + GuidePanel.tsx + GuideStepList.tsx + GuideStepEditor.tsx + GuideSnapshotPreview.tsx + +electron/guide/ + guideStore.ts + guidePaths.ts + guideIpc.ts + ocr/ + paddleOcrClient.ts + ai/ + deepseekGuideClient.ts +``` + +File hiện có khả năng phải sửa: + +- `src/hooks/useScreenRecorder.ts` +- `src/components/launch/LaunchWindow.tsx` +- `src/components/video-editor/VideoEditor.tsx` +- `electron/ipc/handlers.ts` +- `electron/preload.ts` +- file khai báo type cho `window.electronAPI` +- `package.json` nếu thêm script test hoặc dependency nhỏ + +## Artifact Đầu Ra + +Với video `recording-123.mp4`, hệ thống tạo: + +```text +recording-123.mp4 +recording-123.cursor.json +recording-123.guide.json +recording-123-guide/ + step-001.png + step-002.png + ocr.json + guide.md + guide.html +``` + +Quy tắc: + +- `.cursor.json` vẫn là dữ liệu cursor gốc. +- `.guide.json` là source of truth cho guide workflow. +- Folder `recording-123-guide/` chứa file phát sinh từ guide. +- `guide.md` và `guide.html` có thể được tạo lại từ `.guide.json`. + +## Contract Chính + +Tạo `src/guide/contracts.ts`. + +```ts +export type GuideEventKind = "click" | "hotkey" | "manual"; + +export type GuideEventSource = + | "cursor-recording" + | "guide-hotkey" + | "review-ui"; + +export interface GuideEvent { + id: string; + recordingId: string; + kind: GuideEventKind; + source: GuideEventSource; + timeMs: number; + x?: number; + y?: number; + normalizedX?: number; + normalizedY?: number; + button?: "left" | "right" | "middle" | "unknown"; + label?: string; + screenshotOffsetMs?: number; + createdAt: string; +} + +export interface GuideSnapshot { + id: string; + eventId: string; + timeMs: number; + offsetMs: number; + path: string; + width: number; + height: number; +} + +export interface OcrBlock { + id: string; + snapshotId: string; + text: string; + confidence: number; + box: { + x: number; + y: number; + width: number; + height: number; + }; +} + +export interface GuideStepCandidate { + id: string; + eventId: string; + snapshotId?: string; + timeMs: number; + action: "click" | "choose" | "type" | "wait" | "manual"; + targetText?: string; + targetRole?: "button" | "menu" | "tab" | "field" | "link" | "unknown"; + nearbyText: string[]; + confidence: number; +} + +export interface GeneratedGuideStep { + id: string; + order: number; + title: string; + instruction: string; + screenshotPath?: string; + sourceCandidateId?: string; +} + +export interface GeneratedGuide { + title: string; + summary?: string; + steps: GeneratedGuideStep[]; +} + +export interface GuideSession { + schemaVersion: 1; + recordingId: string; + videoPath: string; + cursorPath?: string; + guidePath: string; + outputDir: string; + status: + | "recording" + | "events-ready" + | "snapshots-ready" + | "ocr-ready" + | "draft-ready" + | "reviewed"; + events: GuideEvent[]; + snapshots: GuideSnapshot[]; + ocrBlocks: OcrBlock[]; + candidates: GuideStepCandidate[]; + generatedGuide?: GeneratedGuide; + createdAt: string; + updatedAt: string; +} +``` + +Quy tắc dữ liệu: + +- `timeMs` luôn tính theo timeline video cuối cùng. +- `x/y` là tọa độ pixel nếu có. +- `normalizedX/Y` dùng để chống lệch khi video scale. +- `screenshotOffsetMs` mặc định `500`, nghĩa là lấy ảnh sau click 0.5 giây để bắt trạng thái UI sau thao tác. +- AI output chỉ là draft, user edit mới là nội dung cuối. + +## IPC Cần Thêm + +MVP dùng app-level Electron IPC, không cần đưa vào native bridge vì đây là workflow cấp ứng dụng. + +Preload API đề xuất: + +```ts +window.electronAPI.guide = { + startSession(recordingId: string): Promise; + addMarker(input: AddGuideMarkerInput): Promise; + finalizeEvents(input: FinalizeGuideEventsInput): Promise; + writeSnapshot(input: WriteGuideSnapshotInput): Promise; + runOcr(input: RunGuideOcrInput): Promise; + generateDraft(input: GenerateGuideDraftInput): Promise; + saveGuide(input: SaveGuideInput): Promise; + exportMarkdown(input: ExportGuideInput): Promise<{ path: string }>; + exportHtml(input: ExportGuideInput): Promise<{ path: string }>; +}; +``` + +Input types: + +```ts +export interface AddGuideMarkerInput { + recordingId: string; + timeMs: number; + kind: "hotkey" | "manual"; + label?: string; +} + +export interface FinalizeGuideEventsInput { + recordingId: string; + videoPath: string; + cursorPath?: string; +} + +export interface WriteGuideSnapshotInput { + recordingId: string; + eventId: string; + timeMs: number; + offsetMs: number; + pngBytes: ArrayBuffer; + width: number; + height: number; +} + +export interface RunGuideOcrInput { + recordingId: string; + snapshotIds?: string[]; +} + +export interface GenerateGuideDraftInput { + recordingId: string; + language: "vi" | "en"; + provider: "deepseek" | "local"; +} + +export interface SaveGuideInput { + recordingId: string; + generatedGuide: GeneratedGuide; +} + +export interface ExportGuideInput { + recordingId: string; +} +``` + +## Phase 1: Contracts, Store, IPC + +Mục tiêu: tạo khung lưu trữ `.guide.json` mà chưa đụng recorder. + +Task coding: + +1. Tạo `src/guide/contracts.ts`. +2. Tạo `electron/guide/guidePaths.ts`. +3. Tạo `electron/guide/guideStore.ts`. +4. Tạo `electron/guide/guideIpc.ts`. +5. Register guide IPC trong `electron/ipc/handlers.ts`. +6. Expose API trong `electron/preload.ts`. +7. Bổ sung type cho `window.electronAPI.guide`. + +Yêu cầu kỹ thuật: + +- Ghi file atomically: write temp file rồi rename. +- Validate `schemaVersion`. +- Không throw raw error ra renderer, trả error code ổn định. +- Không yêu cầu AI/OCR trong phase này. + +Acceptance: + +- Tạo được guide session fake bằng IPC. +- Đọc/ghi `.guide.json` round-trip không mất dữ liệu. +- Input thiếu `recordingId` hoặc `videoPath` bị reject rõ ràng. + +Test: + +- `guideStore` tạo path đúng. +- `guideStore` đọc file lỗi schema và trả error. +- IPC handler reject input thiếu field. + +## Phase 2: Build Event Từ Cursor Click + +Mục tiêu: lấy click event từ `.cursor.json` hiện tại. + +Task coding: + +1. Tạo `src/guide/eventBuilder.ts`. +2. Thêm hàm `buildGuideEventsFromCursor`. +3. Lọc sample có `interactionType === "click"`. +4. Convert sang `GuideEvent`. +5. De-duplicate click trong cửa sổ `250ms`. +6. Sort theo `timeMs`. +7. Merge với marker thủ công nếu có. + +Pseudo-code: + +```ts +export function buildGuideEventsFromCursor(input: { + recordingId: string; + samples: CursorRecordingSample[]; + videoWidth?: number; + videoHeight?: number; +}): GuideEvent[] { + const events = input.samples + .filter((sample) => sample.interactionType === "click") + .map((sample) => ({ + id: createGuideEventId(input.recordingId, sample.timeMs), + recordingId: input.recordingId, + kind: "click" as const, + source: "cursor-recording" as const, + timeMs: sample.timeMs, + x: sample.cx, + y: sample.cy, + normalizedX: normalize(sample.cx, input.videoWidth), + normalizedY: normalize(sample.cy, input.videoHeight), + button: "left" as const, + screenshotOffsetMs: 500, + createdAt: new Date().toISOString(), + })); + + return sortGuideEvents(dedupeGuideEvents(events)); +} +``` + +Acceptance: + +- 5 click samples tạo 5 guide events. +- `move` và `mouseup` không tạo step. +- Double click hoặc click bounce không tạo quá nhiều step nếu nằm trong dedupe window. +- Không có cursor click thì vẫn dùng được manual marker. + +Test: + +- convert click sample. +- bỏ qua move/mouseup. +- dedupe theo thời gian. +- sort đúng thứ tự. +- xử lý sample thiếu tọa độ. + +## Phase 3: Guide Mode UI Và Manual Marker + +Mục tiêu: user bật được Guide Mode và đánh dấu bước thủ công. + +Task coding: + +1. Thêm Guide Mode toggle trong `LaunchWindow.tsx`. +2. Truyền trạng thái guide vào flow recording trong `useScreenRecorder.ts`. +3. Khi start recording và Guide Mode on, gọi `guide.startSession(recordingId)`. +4. Thêm nút marker trong HUD. +5. Thêm global hotkey ở Electron main, ví dụ `CommandOrControl+Shift+G`. +6. Khi bấm marker/hotkey, gọi `guide.addMarker`. +7. Khi stop recording, gọi `guide.finalizeEvents`. + +Lưu ý: + +- Global hotkey phải nằm ở Electron main vì app đang được quay có thể đang focus. +- Nếu register hotkey fail, UI vẫn dùng nút marker. +- Không làm thay đổi behavior khi Guide Mode off. + +Acceptance: + +- Guide Mode off: quay/sửa/export vẫn như cũ. +- Guide Mode on: stop recording tạo `.guide.json`. +- Hotkey tạo event đúng timestamp. +- Cancel recording không để lại guide artifact rác. + +## Phase 4: Snapshot Extraction + +Mục tiêu: trích ảnh PNG cho từng event sau khi quay xong. + +Quyết định MVP: + +- Không chụp realtime trong lúc quay. +- Dùng video đã lưu, seek tới timestamp cần lấy. +- Thực hiện trong renderer/editor bằng hidden `