Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ xcuserdata/
*.code-workspace

# Local packages & MCP (separate git repos)
packages/
packages/*
!packages/token-counter-swift/
!packages/token-counter-swift/Package.swift
!packages/token-counter-swift/Sources/
!packages/token-counter-swift/Sources/TokenCounter/
!packages/token-counter-swift/Sources/TokenCounter/**
!packages/token-counter-swift/Tests/
!packages/token-counter-swift/Tests/TokenCounterTests/
!packages/token-counter-swift/Tests/TokenCounterTests/**
!packages/srt-to-html-swift/
!packages/srt-to-html-swift/Package.swift
!packages/srt-to-html-swift/Sources/
Expand Down
37 changes: 37 additions & 0 deletions CONVERSIONS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,48 @@
# macdoc Conversion Matrix

> 平台聲明
> - Token 計數/離線 GPT-4o 層:macOS 27.0(arm64)、Apple Swift 6.3.3;狀態為 `verified`,compiled route 已在 `sandbox-exec` 禁網路與隔離 HOME/cache 下通過 `hello world` exact-byte acceptance;package suite 另通過五組官方 Python `tiktoken` reference vectors。
> - Anthropic token-count transport 層:macOS 27.0(arm64)、Apple Swift 6.3.3;狀態為 `implemented-not-live-verified`,只以注入 transport 驗證 fixed request、response limits、錯誤映射與資料遮蔽,沒有 live provider call。
> - Windows/Linux token 計數:macdoc CLI 與 `TokenCounter` package;狀態為 `not-supported`,目前 manifests 僅宣告 macOS 14+。
> - 證據:`swift test --filter TokenCountCommandTests`、`swift test`(`packages/token-counter-swift`),以及 bundled resource SHA-256 regression。

## Status Legend

| Symbol | Meaning |
|--------|---------|
| ✅ | **implemented** — merged and available |
| · | not planned |

## Token Counting(measurement route,非格式轉換)

`tokens` 接受任何副檔名的一般檔案,但內容必須是完整、嚴格 UTF-8,大小不得超過
1,000,000 bytes。它不透過 matrix 中的中繼格式,也不截斷輸入。

| Model | Invocation | Boundary |
|---|---|---|
| `gpt-4o` | `macdoc convert --to tokens --model gpt-4o sample.txt` | bundled `o200k_base.tiktoken`;固定 SHA-256;完全離線,不下載或寫 cache/HOME |
| `claude-sonnet-4-6` | `macdoc convert --to tokens --model claude-sonnet-4-6 --allow-network sample.txt` | 需要非空 `ANTHROPIC_API_KEY`;把完整檔案文字送到 Anthropic;結果是 provider-reported estimate |
| default(兩者) | `macdoc convert --to tokens --allow-network sample.txt` | 固定 GPT-4o → Claude 順序;兩者都成功才輸出,否則沒有部分 stdout/檔案 |

若 `sample.txt` 精確包含 `hello world`,GPT-4o compiled acceptance 的 stdout bytes 為:

```text
2
```

單模型成功固定輸出 `<ASCII integer>\n`。雙模型成功固定輸出 tab-separated bytes:

```text
Model Tokens
gpt-4o 1234
claude-sonnet-4-6 1198
```

`--output tokens.txt` 只在所有 requested providers 成功後原子寫入;失敗時不存在的目的檔
維持不存在,既有目的檔維持原 bytes。`--allow-network` 是逐次 disclosure consent:環境中有
API key 本身不代表允許送出當次檔案。Anthropic 的 estimate 不應與 bundled GPT-4o 的離線
reference-vector 精確值混為一談。

## Cross Matrix (Source → Target)

| → Target | Markdown | HTML | Word (.docx) | LaTeX | JSON | PDF | SRT |
Expand Down Expand Up @@ -37,6 +73,7 @@
| Markdown → Word | `md-to-word-swift` | ✅ implemented | swift-markdown AST → OOXML writer |
| PDF → DOCX | `pdf-to-docx-swift` | ✅ implemented | PDFKit text extraction → OOXML writer |
| Note → HTML | `note-to-html-swift` | ✅ implemented | Notability .note → interactive HTML player with audio-synced stroke replay |
| UTF-8 text → Token count | `token-counter-swift` | ✅ implemented | measurement route;GPT-4o offline,Claude 僅在逐次同意後連線 |

## Rules

Expand Down
8 changes: 8 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
.package(name: "TokenCounter", path: "packages/token-counter-swift"),
.package(url: "https://github.com/PsychQuant/common-converter-swift.git", from: "0.4.0"),
// Local override — packages/word-to-md-swift has the BodyChild fix
// (`.bookmarkMarker` / `.rawBlockElement` cases) ahead of the published
Expand Down Expand Up @@ -69,16 +70,19 @@ let package = Package(
.product(name: "NoteToHTML", package: "note-to-html-swift"),
.product(name: "NoteToPDF", package: "note-to-pdf-swift"),
.product(name: "OCRCore", package: "ocr-swift"),
.product(name: "TokenCounter", package: "TokenCounter"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.testTarget(
name: "MacDocCLITests",
dependencies: [
"MacDocCLI",
.product(name: "NoteCore", package: "note-core-swift"),
// Authoring API for building synthetic docx fixtures in tests
// (WordReverseCoverageTests → emptyAuthoringDocument).
.product(name: "OOXMLSwift", package: "ooxml-swift"),
.product(name: "TokenCounter", package: "TokenCounter"),
]
),
]
Expand Down
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# macdoc

> 平台聲明
> - Token 計數/離線 GPT-4o 層:macOS 27.0(arm64)、Apple Swift 6.3.3;狀態為 `verified`,compiled executable 已在禁用網路並隔離 HOME/cache 的環境通過 `hello world` exact-byte acceptance;package suite 另以五組官方 Python `tiktoken` vectors 驗證 bundled `o200k_base`。
> - Anthropic token-count transport 層:macOS 27.0(arm64)、Apple Swift 6.3.3;狀態為 `implemented-not-live-verified`,固定 endpoint、錯誤映射與資料遮蔽只以注入 transport 驗證,沒有呼叫 live Anthropic。
> - Windows/Linux token 計數:macdoc CLI 與 `TokenCounter` package;狀態為 `not-supported`,目前 manifests 僅宣告 macOS 14+,不得由離線演算法外推 runtime 支援。
> - 證據:`swift test --filter TokenCountCommandTests`、`swift test`(`packages/token-counter-swift`),以及 `Tests/MacDocCLITests/TokenCountCommandTests.swift`。

原生 macOS 文件處理工具集,專注於文件格式轉換和 OCR。使用 Swift 開發,充分利用 Apple 平台原生能力(PDFKit、Vision.framework)。

## Claude Code Plugin Marketplace
Expand All @@ -25,7 +31,7 @@ MCP plugins 的 wrapper 會自動從各 repo 的 GitHub Releases 下載 universa
## Prerequisites

- **macOS 14+**(Sonoma 或更新)
- **Swift 5.9+**
- **Swift 6.2+**(`swift-tiktoken` 的 package tools requirement)
- **Xcode Command Line Tools**

```bash
Expand Down Expand Up @@ -109,6 +115,54 @@ macdoc convert --to html notes.note --full
macdoc convert --to html notes.note --full --css dark
```

### Token 計數(UTF-8 measurement route)

GPT-4o 使用隨 binary bundled 的 `o200k_base.tiktoken`,先驗證 SHA-256
`446a9538cb6c348e3516120d7c08b09f57c36495e2acfffe59a5bf8b0cfb1a2d`,再於本機計數;
不會下載 vocabulary、連線、寫 tokenizer cache 或寫入 HOME。假設 `sample.txt` 的內容是
`hello world`:

```bash
macdoc convert --to tokens --model gpt-4o sample.txt
```

stdout 的精確 bytes 是一個十進位整數與換行:

```text
2
```

也可寫入檔案;成功時 stdout 保持空白,`tokens.txt` 的內容仍是精確的 `2\n`:

```bash
macdoc convert --to tokens --model gpt-4o --output tokens.txt sample.txt
```

Claude 計數會把**完整、已驗證的輸入文字**送到 Anthropic。每次執行都必須同時提供非空的
`ANTHROPIC_API_KEY` 與明示的 `--allow-network`;API key 只放在 `x-api-key` header:

```bash
export ANTHROPIC_API_KEY='your-key'
macdoc convert --to tokens --model claude-sonnet-4-6 --allow-network sample.txt
```

省略 `--model` 會依固定順序要求 GPT-4o、Claude Sonnet 4.6,因此也需要同一個網路同意與
credential。兩個 provider 都成功後才會一次輸出下列 tab-separated 形狀(數字依檔案與
provider 回應而異):

```text
Model Tokens
gpt-4o 1234
claude-sonnet-4-6 1198
```

限制與語意:

- `--model` 僅接受 `gpt-4o`、`claude-sonnet-4-6`,大小寫必須相符。
- 輸入必須是一般檔案、嚴格 UTF-8、最多 1,000,000 bytes;空檔合法,副檔名不參與判斷。
- Anthropic 數值是該 provider 回報的 input-token estimate,可能隨 provider 行為改變;不是本機可重現的離線精確值。
- 任一 provider 失敗時,stdout 不會出現部分表格,既有 `--output` 檔案也不會被改寫。

常用選項:

| 選項 | 說明 |
Expand All @@ -120,6 +174,8 @@ macdoc convert --to html notes.note --full --css dark
| `--full` | 輸出完整 HTML 文件 |
| `--css dark\|light` | SRT 主題 |
| `--css minimal\|web` | Bib 樣式 |
| `--model gpt-4o\|claude-sonnet-4-6` | token 計數模型(只適用 `--to tokens`) |
| `--allow-network` | 每次允許把完整輸入送到 Anthropic(只適用含 Claude 的 token 計數) |

### BibLaTeX 工具(`bib`)

Expand Down Expand Up @@ -234,6 +290,7 @@ macdoc config ai set transcription codex
| BibLaTeX → Markdown | `convert --to md` |
| BibLaTeX → JSON | `convert --to json` |
| Note → HTML | `convert --to html` |
| UTF-8 text → Token count | `convert --to tokens` |

## MCP Servers

Expand Down
45 changes: 42 additions & 3 deletions Sources/MacDocCLI/MacDoc+Convert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import BibAPAToMD
import MarkerWordConverter
import NoteToHTML
import NoteToPDF
import TokenCounter

// MARK: - Convert 子命令(textutil-compatible 統一入口)
extension MacDoc {
Expand All @@ -26,9 +27,15 @@ extension MacDoc {
abstract: "Convert documents between formats (textutil-compatible)"
)

@Option(name: .long, help: "Target format (md, html, docx, pdf, json, marker)")
var to: String

@Option(name: .long, help: "Target format (md, html, docx, pdf, json, marker, tokens)")
var to: String

@Option(name: .long, help: "Token model: gpt-4o or claude-sonnet-4-6")
var model: String?

@Flag(name: .long, help: "Allow sending the complete input text to a network provider")
var allowNetwork: Bool = false

@Option(name: .long, help: "Output file path (or directory for marker)")
var output: String?

Expand Down Expand Up @@ -59,6 +66,24 @@ extension MacDoc {
let ext = inputURL.pathExtension.lowercased()
let target = to.lowercased()

if target == "tokens" {
try validateTokenRouteOptions()
let outputURL = resolveOutputPath().map(URL.init(fileURLWithPath:))
try await TokenCountCommandRunner.live.execute(
inputURL: inputURL,
modelName: model,
allowNetwork: allowNetwork,
outputURL: outputURL,
stdout: { FileHandle.standardOutput.write($0) },
stderr: { FileHandle.standardError.write($0) }
)
return
}

if model != nil || allowNetwork {
throw ValidationError("--model 與 --allow-network 只適用於 --to tokens")
}

switch (ext, target) {
case ("docx", "md"):
try convertWordToMD(inputURL: inputURL)
Expand Down Expand Up @@ -457,6 +482,20 @@ extension MacDoc {

// MARK: - Helpers

private func validateTokenRouteOptions() throws {
var unsupported: [String] = []
if css != .web { unsupported.append("--css") }
if hardBreaks { unsupported.append("--hard-breaks") }
if full { unsupported.append("--full") }
if frontmatter { unsupported.append("--frontmatter") }
if htmlExtensions { unsupported.append("--html-extensions") }
guard unsupported.isEmpty else {
throw ValidationError(
"--to tokens 不支援格式轉換選項:\(unsupported.joined(separator: ", "))"
)
}
}

/// Resolve the output path: --stdout forces nil (stdout), overriding --output.
/// If neither is specified, defaults to stdout.
private func resolveOutputPath() -> String? {
Expand Down
Loading
Loading