| name | alt2obs |
| description | Import an Alt (altalt.io) lecture into the user's Obsidian vault as page-anchored markdown compatible with the Alt2Obsidian 1.1.0 plugin. Uses Claude Code's native PDF vision (Read with pages parameter) to generate per-slide Korean commentary, sidestepping the Gemini API quota the plugin's full path needs. Output works in the plugin's Synced Viewer. |
alt2obs Skill (Phase 2 Stage A โ Claude Code Max import path)
This Skill produces a page-anchored Obsidian lecture note from an Alt URL. The output is byte-compatible with Alt2Obsidian 1.1.0's storage format (## ๐ ์ฌ๋ผ์ด๋ N sections, <!-- alt2obs:slide:N hash:H start --> ... <!-- end --> managed markers, > [!note] ๋ด ๋ฉ๋ชจ callouts), so the plugin's Synced Viewer renders it correctly and re-imports preserve user free-space via the multi-managed merge.
The Skill exists because the plugin's per-slide Gemini multimodal call hits free-tier RPD limits on long decks. This path uses Claude Code's own session vision instead.
When to use
- User provides an Alt URL and asks to "import" / "alt2obs" / "Phase 2 import" / "Claude Code ๋ฒ์ ์ผ๋ก import".
- Gemini quota is exhausted or the user wants Claude commentary quality.
- User wants a one-off import without waiting for plugin's per-slide rate-limited loop.
Required inputs
Parse from the user's message (or ask if missing):
| Input | Example | Required |
|---|
url | https://altalt.io/note/b7472c41-โฆ | yes |
vault | absolute path, e.g. /Users/biqnt/Documents/lecture-vault | yes โ read from ~/Library/Application Support/obsidian/obsidian.json if a single vault, else ask |
subject | folder under Alt2Obsidian/, e.g. CSED232 | yes โ ask if not in user's message |
title | filename stem, e.g. 8๊ฐ | optional โ falls back to scraped Alt note title |
period | midterm / final (or Korean equivalents โ see mapping below) | optional โ required if the user wants this lecture to appear in the plugin's "์ํ๋๋น ์์ฝ" extraction |
Exam-period tag mapping (CRITICAL โ the plugin's VaultManager.readNotesForSubject filters with strict English match):
| User says | Map to tag |
|---|
midterm, ์ค๊ฐ, ์ค๊ฐ๊ณ ์ฌ, ์ค๊ฐ๊ณ ์ฌ๋ฒ์ | midterm |
final, ๊ธฐ๋ง, ๊ธฐ๋ง๊ณ ์ฌ, ๊ธฐ๋ง๊ณ ์ฌ๋ฒ์ | final |
| (omitted) | no period tag โ note will not appear in exam-summary extraction |
The plugin checks tags.includes("midterm") or tags.includes("final") literally. Writing the Korean phrase verbatim (e.g., ๊ธฐ๋ง๊ณ ์ฌ๋ฒ์) breaks the filter. Always normalize to the English value before adding to the frontmatter tags: array.
Workflow
1. Scrape Alt metadata
Run the scraper helper. The repo lives at /Users/biqnt/dev_project/alt2obsidian (adjust if invoked elsewhere).
node /Users/biqnt/dev_project/alt2obsidian/scripts/phase2/alt-scrape.mjs "<url>"
Stdout is a single-line JSON object: {title, summary, pdfUrl, transcript, noteId, createdAt, parseQuality}. Capture and parse it.
If parseQuality === "partial" or pdfUrl === null, stop and tell the user โ Phase 2 needs the PDF.
2. Download the PDF
curl -sSL -o "/tmp/alt-deck-<noteId>.pdf" "<pdfUrl>"
Quote the URL (it has & query params). Verify the file is non-empty (ls -l).
3. Read each slide and compose commentary
Use Read with the pages parameter to walk through the deck, 20 pages at a time (the tool's max). Example:
Read(file_path="/tmp/alt-deck-<noteId>.pdf", pages="1-20")
Read(file_path="/tmp/alt-deck-<noteId>.pdf", pages="21-40")
โฆ
Reading a PDF returns the page contents as images you can see directly. For each page N, write a Korean commentary section following these rules (these mirror the plugin's PerSlideCommentaryGenerator prompt โ match the tone so plugin and Skill outputs feel uniform):
- Output is the section body only โ never include the section heading (
## โฆ); the assembler step adds it.
- 200โ500 ํ๊ธ ๋ฌธ์.
- ์ ์/๊ฐ๋
:
> [!definition] ๊ฐ๋
๋ช
callout.
- ์์/๊ณต์/์ฝ๋:
> [!example] callout.
- ์ํ ์ถ์ ํฌ์ธํธ:
> [!important] callout.
- ์์ฑ ์ ์ฌ๊ฐ ์์ผ๋ฉด (see step 4) ๊ต์๋ ๊ฐ์กฐ 1โ2 ํฌ์ธํธ๋ง ์ธ์ฉ ํํ๋ก โ raw ๋คํ ๊ธ์ง.
- ํต์ฌ ๊ฐ๋
์
[[๊ฐ๋
๋ช
]] wikilink (๊ด๋ จ ๊ฐ๋
์ด ๋ฐ๋ณต๋๋ฉด ๋ชจ๋ ์ฌ๋ผ์ด๋์์ ์ผ๊ด๋๊ฒ wrap).
- ํ์ง/๋ชฉ์ฐจ/Thank you ๊ฐ์ ๋น์ค์ง ์ฌ๋ผ์ด๋๋ ํ ์ค๋ก ๊ฐ๋จํ.
4. (optional) Curate transcript per slide
If transcript is non-empty, split it evenly by character count across the slide count and pass the chunk that corresponds to slide N as additional context for the commentary. The transcript is from Alt's audio capture; lecturers' verbal asides go here. Even-split is the same heuristic the plugin uses โ segment timestamps are dropped by Alt's RSC payload.
5. Compute the slide hash
For each page N, compute sha1("{noteId}:{N}").slice(0, 8). In Bash:
HASH=$(printf "%s:%d" "<noteId>" "<N>" | shasum -a 1 | cut -c1-8)
This is deterministic but different from the plugin's hash (the plugin hashes the rendered PNG bytes; the Skill cannot reproduce that hash without a Node canvas dependency). Document this caveat in the writeup at the bottom โ see "Hash compat caveat" below.
6. Assemble the markdown
---
title: "<title>"
subject: "<subject>"
tags: [<subject lowercased>, <englishPeriodIfSpecified>, <conceptTag1>, <conceptTag2>, ...]
date: "<YYYY-MM-DD>"
source: "alt2obsidian-cc-skill"
slide_count: <N>
alt_id: "<noteId>"
alt_created: "<createdAt>"
---
# <title>
## ๐ ์ฌ๋ผ์ด๋ 1
<!-- alt2obs:slide:1 hash:<8-hex> start -->
<commentary for slide 1>
<!-- alt2obs:slide:1 hash:<8-hex> end -->
> [!note] ๋ด ๋ฉ๋ชจ
>
## ๐ ์ฌ๋ผ์ด๋ 2
<!-- alt2obs:slide:2 hash:<8-hex> start -->
<commentary for slide 2>
<!-- alt2obs:slide:2 hash:<8-hex> end -->
> [!note] ๋ด ๋ฉ๋ชจ
>
โฆ (repeat for all N slides) โฆ
Marker format must match exactly: <!-- alt2obs:slide:N hash:HHHHHHHH start --> (with single spaces) โ this is what VaultManager.splitMultiManagedNote parses.
6.5 Extract concepts (NEW โ required for parity with the plugin)
After all per-slide commentary is written and the assembled lecture markdown is in your buffer, run a final pass to extract academic concepts from the whole lecture. This produces separate Concepts/<name>.md files that the lecture's [[wikilinks]] resolve to โ without this step the wikilinks dangle.
Concept extraction prompt (mirror this exactly โ same quality bar as the plugin's ConceptExtractor):
LANGUAGE: ๋ชจ๋ concept ํ๋ (definition, lectureContext, example, caution)๋ ํ๊ตญ์ด๋ก. ์์ด ์ค๋ช
์ ํ๊ตญ์ด ํ๋์ ์์ง ๋ง. ๋จ, ์ ๋ฌธ ์ฉ์ด๋ ๊ดํธ๋ก ์์ด ๋ณ๊ธฐ OK (์: **๋ช
์ธ(Specification)**).
For each concept provide:
- name: 1-4 words. ํ๊ตญ์ด + ์์ด ๋ณ๊ธฐ ํจํด "ํ๊ตญ์ด (English)", e.g. "๋ฐ์ดํฐ ์ถ์ํ (Data Abstraction)".
- definition: 3-5 sentences. ๊ฐ๋
์ด ๋ฌด์์ด๊ณ , ๋ฌด์๊ณผ ๊ตฌ๋ณ๋๋ฉฐ, ์ด ๊ฐ์ ๋งฅ๋ฝ์์ ์ ์ค์ํ์ง ๋ค ๋ค๋ค. ํ ์ค์ง๋ฆฌ ์ ์๋ ๊ฑฐ๋ถ.
- lectureContext: 2-3 sentences. ์ด๋ฒ ๊ฐ์์์ ์ด๋ป๊ฒ ๋์
๋๊ณ ์ฌ์ฉ๋์๋์ง. "๊ต์๋์ด ์ด ์ฌ๋ผ์ด๋์์ X๋ฅผ ์ค๋ช
ํ๊ธฐ ์ํด ๋์
ํ๋ค", "์ ๊ฐ์์ Y์ ๋๋นํด ์๊ฐ๋์๋ค"๊ฐ์ด narrative์ ์ฐ๊ฒฐ.
- example: ๊ฐ์์ ์๋ ๊ตฌ์ฒด์ ์์ (์ซ์, ์ฝ๋, ๊ณต์, ํน์ ์ผ์ด์ค). 2-3 sentences. ๊ฐ์์ ์ง์ง ์์๊ฐ ์์ผ๋ฉด ๋น์๋ฌ.
- caution: ํ์์ด ์์ฃผ ํ๋ ์ค์, ์ํ ํจ์ , ๋ฏธ๋ฌํ ๊ตฌ๋ถ. ์ง์ง ๋ ์ค๋ฅด์ง ์์ผ๋ฉด ๋น์๋ฌ โ ํจ๋ฉํ์ง ๋ง.
- relatedConcepts: ๊ฐ์ ๊ฐ์์์ ์ถ์ถํ ๋ค๋ฅธ concept ์ด๋ฆ๋ค (๋๋ ๊ธฐ์กด concepts/ ํด๋์ ์๋ ์ด๋ฆ๋ค). 2๊ฐ ์ด์์ concept์ ์ถ์ถํ์ผ๋ฉด ๋ชจ๋ concept์ ์ ์ด๋ 1๊ฐ์ relatedConcept์ ๊ฐ์ ธ์ผ ํด. ์ ํํ ์ด๋ฆ ์ฌ์ฉ, ์ ์ด๋ฆ ๋ง๋ค์ง ๋ง.
๊ธฐ์กด concepts/ ํด๋์ ๊ฐ์ ์๋ฏธ์ ๋
ธํธ๊ฐ ์์ผ๋ฉด ๊ทธ ์ ํํ ์ด๋ฆ์ ์ฌ์ฌ์ฉ โ concept ๊ทธ๋ํ ๋ถ์ด ๋ฐฉ์ง.
๋ถ๋: 4-8 concepts ์ถ์ถ (๋ณดํต ๊ฐ์ ๊ธฐ์ค). ์ข์ ๊ฐ์๋ฉด ๋ ์ ๊ฒ, ๊ด๋ฒ์ํ surveys๋ฉด ๋ ๋ง์ด. ํจ๋ฉ ๊ธ์ง.
Workflow:
-
List existing concept names by globbing <vault>/Alt2Obsidian/<subject>/Concepts/*.md (use Bash ls). These are reuse candidates.
-
Apply the prompt above to the assembled lecture content. Output JSON shape:
{
"concepts": [
{
"name": "๋ฐ์ดํฐ ์ถ์ํ (Data Abstraction)",
"definition": "...",
"lectureContext": "...",
"example": "...",
"caution": "...",
"relatedConcepts": ["๊ฐ์ฒด ๋ช
์ธ (Object Specification)", "..."]
}
],
"tags": ["..."]
}
-
For each concept, generate a markdown file at <vault>/Alt2Obsidian/<subject>/Concepts/<sanitized-name>.md using the plugin's exact template (mirror src/vault/VaultManager.ts:250-277):
---
tags: [concept]
---
# {name}
**์ ์:** {definition}
**๊ฐ์ ๋งฅ๋ฝ:** {lectureContext}
**์์:** {example}
**์ฃผ์:** {caution}
**๊ด๋ จ ๊ฐ์:** [[{lectureTitle}]]
**๊ด๋ จ ๊ฐ๋
:** [[{relatedConcept1}]], [[{relatedConcept2}]]
Skip the **์์:** line entirely if example is empty; same for **์ฃผ์:** and **๊ด๋ จ ๊ฐ๋
:**. Do NOT emit empty-value lines โ match how the plugin elides them.
-
Skip-if-exists with append behaviour: if <vault>/Alt2Obsidian/<subject>/Concepts/<sanitized-name>.md already exists from a prior import:
- Read it.
- If
**๊ด๋ จ ๊ฐ์:** already contains [[{lectureTitle}]], leave the file untouched.
- Otherwise append
, [[{lectureTitle}]] to the existing **๊ด๋ จ ๊ฐ์:** line. This matches VaultManager.appendLectureReference (src/vault/VaultManager.ts:295-310) โ same lecture cross-linking semantics.
- Optionally enrich missing fields (e.g., the prior concept note has no
**์์:** and the new lecture has a good one) by appending the new field above the **๊ด๋ จ ๊ฐ์:** line. Mirrors VaultManager.appendMissingConceptField (:312-324).
-
Filename sanitization: replace /, \, :, ?, *, ", <, >, | with _ (mirrors src/utils/helpers.ts:sanitizeFilename). Korean characters and parentheses are valid in vault filenames.
-
After writing all concept notes, append a brief summary in the completion message: "{N} concept notes written to Concepts/ โ {a few names}".
7. Write to the vault
mkdir -p "<vault>/Alt2Obsidian/<subject>"
Then Write the assembled markdown to:
<vault>/Alt2Obsidian/<subject>/<title>.md
And copy the PDF to its sibling location (Task 1.4 layout):
cp "/tmp/alt-deck-<noteId>.pdf" "<vault>/Alt2Obsidian/<subject>/<title>.pdf"
8. Report completion
Tell the user: file path written, slide count, any slides where you found the content was unusually thin (e.g. a totally blank slide), and a one-line note that the Synced Viewer can be opened from Obsidian's command palette.
Hash compat caveat (always include in completion message)
The hash field uses SHA-1("{noteId}:{slideNum}").slice(0, 8). This is deterministic per (lecture, slide) โ re-running the Skill on the same Alt URL produces matching hashes, so the Round 5 invariant (per-slide free-space preservation across regen) holds within Skill outputs. But the plugin's Gemini path uses SHA-1(rendered_PNG_bytes).slice(0, 8), which is different. If the user later re-imports the same lecture via the plugin, every section will surface as slideDrift once โ memos are still preserved (via the N-match-with-drift branch), but a confirmation modal will list every slide as "drifted." A future Stage B (real monorepo + node-canvas) can produce the same byte-hashes as the plugin and eliminate this.
Retroactive fix for existing Skill-generated notes
If you already imported a lecture via the Skill before this exam-period fix, the note's tags: line may contain Korean strings like ๊ธฐ๋ง๊ณ ์ฌ๋ฒ์ instead of the English final. The plugin's exam-summary extractor will skip those notes. To fix, run:
sed -i '' 's/๊ธฐ๋ง๊ณ ์ฌ๋ฒ์/final/g; s/๊ธฐ๋ง๊ณ ์ฌ/final/g; s/์ค๊ฐ๊ณ ์ฌ๋ฒ์/midterm/g; s/์ค๊ฐ๊ณ ์ฌ/midterm/g' "<vault>/Alt2Obsidian/<subject>/<title>.md"
Or just re-run the Skill against the same Alt URL with period=final (or midterm) โ the multi-managed merge preserves your > [!note] ๋ด ๋ฉ๋ชจ callouts via the hash-match path, and the corrected tag gets written.
Error handling
alt-scrape.mjs exits 1 with stderr message โ relay to user, stop.
parseQuality: "partial" or pdfUrl: null โ tell user the Alt note isn't a full lecture and stop.
Read of a PDF page fails โ log the slide as ## โ ๏ธ ์ฒ๋ฆฌ ์คํจ ์ฌ๋ผ์ด๋ N footer at the end of the markdown (matches the plugin's failure-footer convention), continue with the rest.
- Vault path doesn't exist โ ask the user; do NOT create it without consent.
- A file already exists at the target
.md path โ tell the user and ask before overwriting (matches the plugin's confirm-on-update flow).
Out of scope (Phase 2 Stage B)
- Hash compat with plugin (needs node-canvas + same pdfjs render path).
- npm-publishable CLI. The script is repo-local for now.
- requestUrl decoupling in the plugin's
src/llm/, src/scraper/, src/pdf/ (Task 2.3).
- Cross-platform PDF.js spike (Task 2.5).
Quick test target
The user's existing 8๊ฐ URL should work end-to-end:
url: https://altalt.io/note/b7472c41-f585-4109-a076-2d8925dd9e7d
vault: /Users/biqnt/Documents/lecture-vault
subject: 8๊ฐ
title: 8๊ฐ-claude
This avoids overwriting the existing 8๊ฐ.md (Gemini-generated). Compare side-by-side after import to evaluate Skill commentary quality vs Gemini's.