diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index cb910b5..29e3bfe 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -23,7 +23,7 @@ jobs: if: ${{ steps.filter.outputs.py_modified == 'true' }} uses: actions/setup-python@v2 with: - python-version: 3.10.0 + python-version: "3.10" architecture: x64 cache: 'pip' - name: Install dependencies diff --git a/course-115/.hackmd-sync.json b/course-115/.hackmd-sync.json new file mode 100644 index 0000000..8c5565f --- /dev/null +++ b/course-115/.hackmd-sync.json @@ -0,0 +1,158 @@ +{ + "api_base": "https://api.hackmd.io/v1", + "token_env": "HACKMD_API_TOKEN", + "materials_root": "weeks", + "manifest_path": "hackmd.book.md", + "book_note_id": "8PS1YcDJSdGJQuwTwuScdg", + "default_permissions": { + "readPermission": "guest", + "writePermission": "owner", + "commentPermission": "disabled" + }, + "notes": { + "weeks/W01_課程啟動與多媒體系統.md": { + "note_id": "IpvhLzxDR4O6OPQhEjogxw", + "title": "W01|先拆解,再創作:多媒體系統與 HackMD", + "shortId": "SJG50wvLzl", + "publishLink": "https://hackmd.io/@wiimax/SJG50wvLzl", + "created_from": "weeks/W01_課程啟動與多媒體系統.md", + "last_local_push": 1786375820 + }, + "weeks/W02_數位影像與像素.md": { + "note_id": "cPhk_js3Q0GoqqvjOCcw7Q", + "title": "W02|一張圖片在電腦裡是什麼?", + "shortId": "HJNm5CvwLzx", + "publishLink": "https://hackmd.io/@wiimax/HJNm5CvwLzx", + "created_from": "weeks/W02_數位影像與像素.md", + "last_local_push": 1786375821 + }, + "weeks/W03_卷積與影像處理.md": { + "note_id": "i5Ezqm8fTfWnHUfAGol1qQ", + "title": "W03|濾鏡為什麼能找出邊緣?", + "shortId": "S1V45CwPIfg", + "publishLink": "https://hackmd.io/@wiimax/S1V45CwPIfg", + "created_from": "weeks/W03_卷積與影像處理.md", + "last_local_push": 1786375822 + }, + "weeks/W04_擴散模型原理.md": { + "note_id": "acOyEDr1Q6WiqMPomutPjA", + "title": "W04|AI 如何從雜訊生成圖片?", + "shortId": "SJHqRwvIMx", + "publishLink": "https://hackmd.io/@wiimax/SJHqRwvIMx", + "created_from": "weeks/W04_擴散模型原理.md", + "last_local_push": 1786375822 + }, + "weeks/W05_潛在擴散與提示控制.md": { + "note_id": "Acqf1hjfQuWLqqHFvhcahw", + "title": "W05|文字如何控制生成影像?", + "shortId": "SkUqAPv8Me", + "publishLink": "https://hackmd.io/@wiimax/SkUqAPv8Me", + "created_from": "weeks/W05_潛在擴散與提示控制.md", + "last_local_push": 1786375823 + }, + "weeks/W06_LoRA與影像微調.md": { + "note_id": "JhvO9LwVQqKOS7u77jvFSw", + "title": "W06|影像微調到底改了什麼?", + "shortId": "BkHL9RPv8Ge", + "publishLink": "https://hackmd.io/@wiimax/BkHL9RPv8Ge", + "created_from": "weeks/W06_LoRA與影像微調.md", + "last_local_push": 1786375824 + }, + "weeks/W07_生成影像工作室.md": { + "note_id": "5OZp57CNRFiIO-0d3ywEkg", + "title": "W07|生成影像工作室:一致性比單張漂亮更重要", + "shortId": "r1wcCwwUfl", + "publishLink": "https://hackmd.io/@wiimax/r1wcCwwUfl", + "created_from": "weeks/W07_生成影像工作室.md", + "last_local_push": 1786375825 + }, + "weeks/W08_期中影像展與技術答辯.md": { + "note_id": "e9hggKDASSaGlDJGA8s6kQ", + "title": "W08|期中影像展:把「我覺得好看」變成可說明的技術判斷", + "shortId": "r1uqCvwIfx", + "publishLink": "https://hackmd.io/@wiimax/r1uqCvwIfx", + "created_from": "weeks/W08_期中影像展與技術答辯.md", + "last_local_push": 1786375826 + }, + "weeks/W09_期中考週.md": { + "note_id": "sKMpUAl0St-eotC-F6WtdA", + "title": "W09|期中考週:不授新課", + "shortId": "HyFcRPvLzx", + "publishLink": "https://hackmd.io/@wiimax/HyFcRPvLzx", + "created_from": "weeks/W09_期中考週.md", + "last_local_push": 1786375826 + }, + "weeks/W10_數位聲音與頻譜.md": { + "note_id": "-MOYCr5MR42TVJo2cS8Z-Q", + "title": "W10|聲音如何變成可以運算的數字?", + "shortId": "S1qcRvP8Mg", + "publishLink": "https://hackmd.io/@wiimax/S1qcRvP8Mg", + "created_from": "weeks/W10_數位聲音與頻譜.md", + "last_local_push": 1786375827 + }, + "weeks/W11_聲音剪輯與訊號處理.md": { + "note_id": "HeMA4-uFTRepSHzwjg508A", + "title": "W11|剪輯聲音時,實際改變了什麼?", + "shortId": "ry59CDwUGg", + "publishLink": "https://hackmd.io/@wiimax/ry59CDwUGg", + "created_from": "weeks/W11_聲音剪輯與訊號處理.md", + "last_local_push": 1786375828 + }, + "weeks/W12_語音合成與深偽辨識.md": { + "note_id": "7xe8MLQjRjaqcjTGn_uZ7g", + "title": "W12|聲音深偽如何生成,又如何辨識風險?", + "shortId": "SyEj9AwPUfx", + "publishLink": "https://hackmd.io/@wiimax/SyEj9AwPUfx", + "created_from": "weeks/W12_語音合成與深偽辨識.md", + "last_local_push": 1786375828 + }, + "weeks/W13_短影音腳本與數位影片.md": { + "note_id": "b2Im7ebKTDKH8ojQvcQNTQ", + "title": "W13|短影音不是把素材排在一起而已嗎?", + "shortId": "Bk39CDwUMg", + "publishLink": "https://hackmd.io/@wiimax/Bk39CDwUMg", + "created_from": "weeks/W13_短影音腳本與數位影片.md", + "last_local_push": 1786375829 + }, + "weeks/W14_生成內容與素材溯源.md": { + "note_id": "ZpTR4OlhQAWWjYH-KK7geA", + "title": "W14|如何建立可追溯的多媒體素材庫?", + "shortId": "Bk2qADw8fg", + "publishLink": "https://hackmd.io/@wiimax/Bk2qADw8fg", + "created_from": "weeks/W14_生成內容與素材溯源.md", + "last_local_push": 1786375830 + }, + "weeks/W15_FFmpeg與短影音合成.md": { + "note_id": "TOgQVBQBRz6LIEatAAoItQ", + "title": "W15|影片如何被合成與壓縮?", + "shortId": "BkEpq0wPUGx", + "publishLink": "https://hackmd.io/@wiimax/BkEpq0wPUGx", + "created_from": "weeks/W15_FFmpeg與短影音合成.md", + "last_local_push": 1786375830 + }, + "weeks/W16_進度小報告與系統測試.md": { + "note_id": "3k08HgBySd6T0gi2nPsGSA", + "title": "W16|進度小報告:作品是否已經成為一個系統?", + "shortId": "SJRqCPDIzl", + "publishLink": "https://hackmd.io/@wiimax/SJRqCPDIzl", + "created_from": "weeks/W16_進度小報告與系統測試.md", + "last_local_push": 1786375831 + }, + "weeks/W17_期末成果發表A.md": { + "note_id": "W5udh8xkSeWP451yfNfOiA", + "title": "W17|期末成果發表 A", + "shortId": "Sy09APPUzx", + "publishLink": "https://hackmd.io/@wiimax/Sy09APPUzx", + "created_from": "weeks/W17_期末成果發表A.md", + "last_local_push": 1786375832 + }, + "weeks/W18_期末成果發表B與封存.md": { + "note_id": "EvnBXWrZSrCveEPIMek-IQ", + "title": "W18|期末成果發表 B:把作品留下來,也把判斷留下來", + "shortId": "BkkiCwwLfe", + "publishLink": "https://hackmd.io/@wiimax/BkkiCwwLfe", + "created_from": "weeks/W18_期末成果發表B與封存.md", + "last_local_push": 1786375833 + } + } +} diff --git a/course-115/COURSE_HOME.md b/course-115/COURSE_HOME.md new file mode 100644 index 0000000..7c4ba2d --- /dev/null +++ b/course-115/COURSE_HOME.md @@ -0,0 +1,29 @@ +# 多媒體系統 115|課程教材中心 + +本資料夾是日間部與進修部共用的新版教材來源。課程主軸不是介紹雲端平台,而是理解多媒體系統背後的資料、演算法與製作管線,並用 AI 協作完成一支短影音作品。 + +## 課程入口 + +- 日間部:[Thu 多媒體系統115(日)資工一](https://hackmd.io/@wiimax/SJemnYy8fg) +- 進修部:[Sat 多媒體系統115(夜)資工一](https://hackmd.io/@wiimax/ryNWHeqSzg) +- 程式參考:[MediaSystem-Python-Course](https://github.com/willismax/MediaSystem-Python-Course) + +## 資料夾 + +- `weeks/`:W01–W18 學生版 HackMD 講義。 +- `labs/`:CPU 與 Colab 可執行的程式實驗。 +- `assets/`:以 ChatGPT Image 產生的教學圖像與授權紀錄。 +- `slides/`:每週 16:9 PPTX 與預覽圖。 +- `notebooklm/`:NotebookLM 來源清單、產物資訊與匯出檔。 +- `templates/`:AI 使用紀錄、素材資料卡、同意紀錄與評量規準。 + +## 使用原則 + +1. 兩班共用同一份正文,班級首頁只維護日期、公告與期限。 +2. 講義內的「日間引導」與「進修挑戰」不是能力標籤,而是不同的時間配置與任務支架。 +3. 每個必做實驗都有 CPU 路徑;Colab GPU 與商業服務只能是選配。 +4. GitHub 保存可版本控制的來源;HackMD 作為學生閱讀與課堂互動入口。 +5. 每次使用 AI,都要留下提示、參數、採用/拒絕理由與驗證方式。 + +完整進度與評量請見 [共同課綱](common-syllabus-2026.md)。 + diff --git a/course-115/DELIVERY_INDEX.md b/course-115/DELIVERY_INDEX.md new file mode 100644 index 0000000..8155a1b --- /dev/null +++ b/course-115/DELIVERY_INDEX.md @@ -0,0 +1,37 @@ +# 多媒體系統 115|交付索引 + +## 線上入口 + +- 日間部首頁: +- 進修部首頁: +- W01–W18 HackMD 總目錄: +- GitHub 教材分支: + +兩班首頁都已保留原內容,並在末端加入同一個共用教材區塊。 + +## 本機交付 + +| 類型 | 位置 | 數量/說明 | +|---|---|---| +| 學生講義 | `weeks/` | W01–W18,共 18 份 Markdown | +| 每週簡報 | `slides/` | W01–W18,共 18 份 PPTX | +| CPU 程式 | `labs/` | 影像、擴散、聲音、短影音共 6 份 | +| Colab | `colab/` | 對應 6 份 Notebook | +| ChatGPT 生成圖 | `assets/diagrams/` | 7 張原始生成構圖、15 個課程引用檔名 | +| NotebookLM | `notebooklm/` | 來源清單、study guide、21 頁官方總覽 PPTX | +| 評量與紀錄 | `templates/` | 素材表、同意紀錄、期末規準、AI 使用紀錄 | + +## 已確認的週次安排 + +- W08:期中影像作品與技術答辯。 +- W09:期中考週,不授新課。 +- W16:Rough cut、架構圖與三類測試的小型進度發表。 +- W17–W18:期末成果發表、個人口頭問答與專題封存。 + +## 產製與驗證 + +- 18 份週簡報以 16:9 版面產生,已由本機 PowerPoint 全數開啟、匯出並檢查縮圖。 +- Python 腳本已通過 `compileall`;6 份 Colab Notebook 由相同程式來源建立。 +- HackMD 先以 dry-run 確認 18 份皆為新筆記,再執行上傳;同步設定不含 API Token。 +- 圖像生成紀錄見 `assets/GENERATED_IMAGES.md`;精確公式與技術限制仍以講義文字為準。 + diff --git a/course-115/assets/GENERATED_IMAGES.md b/course-115/assets/GENERATED_IMAGES.md new file mode 100644 index 0000000..fffea13 --- /dev/null +++ b/course-115/assets/GENERATED_IMAGES.md @@ -0,0 +1,21 @@ +# 教學圖像生成紀錄 + +本資料夾內的教學圖像於 2026-08-10 透過 Codex 內建的 ChatGPT 圖像生成工具建立。原始提示要求 16:9、深色高對比、技術流程可視化、無品牌與無真人。精確名詞、公式與限制以每週講義文字為準,圖像只作概念導引。 + +| 原始生成檔 | 教材用途 | 對應檔案 | +|---|---|---| +| `exec-5126dceb-9f90-4086-bb13-87e8668265a4.png` | 課程四階段總覽 | `course-map.png` | +| `exec-d3d905d5-5a9f-4db7-a0c9-7176f4c5a55a.png` | 像素、RGB 通道與卷積 | `pixel-rgb.png`、`convolution.png` | +| `exec-3e707e11-6f51-40cb-9577-6d20f8d4661b.png` | DDPM 前向加噪與反向去噪 | `diffusion-process.png` | +| `exec-a4ee4592-e86d-4814-a27a-fae2f4e0306d.png` | 潛在擴散與 LoRA 低秩更新 | `latent-diffusion.png`、`lora.png` | +| `exec-17fc1d5e-e44d-4e37-8713-96e317e085e2.png` | 影像一致性、版本篩選與展示 | `image-story-workflow.png`、`image-showcase.png` | +| `exec-1296837f-218b-41a0-8308-bbafc8155b10.png` | 取樣、STFT 與聲音處理 | `audio-sampling-stft.png`、`audio-editing.png` | +| `exec-713fa584-f5c2-4283-bd2e-f0be73bd5ffd.png` | TTS、聲紋條件、揭露與多證據查證 | `voice-synthesis-safety.png` | +| `exec-1603904a-4343-402e-aebc-c3b4b0134d03.png` | 分鏡、素材溯源、時間軸、GOP 與編碼輸出 | `short-video-storyboard.png`、`media-provenance.png`、`video-codec-gop.png`、`final-project-pipeline.png` | + +## 使用提醒 + +- 圖中數字是流程順序,不是固定參數。 +- 生成圖可能為了可視性簡化比例與連線,不能取代論文架構或實際程式輸出。 +- 課堂引用圖片時,請保留生成內容揭露;不要把示意畫面當成真實量測結果。 + diff --git a/course-115/assets/diagrams/audio-editing.png b/course-115/assets/diagrams/audio-editing.png new file mode 100644 index 0000000..3416f7a Binary files /dev/null and b/course-115/assets/diagrams/audio-editing.png differ diff --git a/course-115/assets/diagrams/audio-sampling-stft.png b/course-115/assets/diagrams/audio-sampling-stft.png new file mode 100644 index 0000000..3416f7a Binary files /dev/null and b/course-115/assets/diagrams/audio-sampling-stft.png differ diff --git a/course-115/assets/diagrams/convolution.png b/course-115/assets/diagrams/convolution.png new file mode 100644 index 0000000..97c0bf9 Binary files /dev/null and b/course-115/assets/diagrams/convolution.png differ diff --git a/course-115/assets/diagrams/course-map.png b/course-115/assets/diagrams/course-map.png new file mode 100644 index 0000000..5704450 Binary files /dev/null and b/course-115/assets/diagrams/course-map.png differ diff --git a/course-115/assets/diagrams/diffusion-process.png b/course-115/assets/diagrams/diffusion-process.png new file mode 100644 index 0000000..adedd77 Binary files /dev/null and b/course-115/assets/diagrams/diffusion-process.png differ diff --git a/course-115/assets/diagrams/final-project-pipeline.png b/course-115/assets/diagrams/final-project-pipeline.png new file mode 100644 index 0000000..b619b7c Binary files /dev/null and b/course-115/assets/diagrams/final-project-pipeline.png differ diff --git a/course-115/assets/diagrams/image-showcase.png b/course-115/assets/diagrams/image-showcase.png new file mode 100644 index 0000000..706d571 Binary files /dev/null and b/course-115/assets/diagrams/image-showcase.png differ diff --git a/course-115/assets/diagrams/image-story-workflow.png b/course-115/assets/diagrams/image-story-workflow.png new file mode 100644 index 0000000..706d571 Binary files /dev/null and b/course-115/assets/diagrams/image-story-workflow.png differ diff --git a/course-115/assets/diagrams/latent-diffusion.png b/course-115/assets/diagrams/latent-diffusion.png new file mode 100644 index 0000000..4efe6d5 Binary files /dev/null and b/course-115/assets/diagrams/latent-diffusion.png differ diff --git a/course-115/assets/diagrams/lora.png b/course-115/assets/diagrams/lora.png new file mode 100644 index 0000000..4efe6d5 Binary files /dev/null and b/course-115/assets/diagrams/lora.png differ diff --git a/course-115/assets/diagrams/media-provenance.png b/course-115/assets/diagrams/media-provenance.png new file mode 100644 index 0000000..b619b7c Binary files /dev/null and b/course-115/assets/diagrams/media-provenance.png differ diff --git a/course-115/assets/diagrams/pixel-rgb.png b/course-115/assets/diagrams/pixel-rgb.png new file mode 100644 index 0000000..97c0bf9 Binary files /dev/null and b/course-115/assets/diagrams/pixel-rgb.png differ diff --git a/course-115/assets/diagrams/short-video-storyboard.png b/course-115/assets/diagrams/short-video-storyboard.png new file mode 100644 index 0000000..b619b7c Binary files /dev/null and b/course-115/assets/diagrams/short-video-storyboard.png differ diff --git a/course-115/assets/diagrams/video-codec-gop.png b/course-115/assets/diagrams/video-codec-gop.png new file mode 100644 index 0000000..b619b7c Binary files /dev/null and b/course-115/assets/diagrams/video-codec-gop.png differ diff --git a/course-115/assets/diagrams/voice-synthesis-safety.png b/course-115/assets/diagrams/voice-synthesis-safety.png new file mode 100644 index 0000000..b470e5e Binary files /dev/null and b/course-115/assets/diagrams/voice-synthesis-safety.png differ diff --git a/course-115/colab/W02_Image_Basics.ipynb b/course-115/colab/W02_Image_Basics.ipynb new file mode 100644 index 0000000..7071bdd --- /dev/null +++ b/course-115/colab/W02_Image_Basics.ipynb @@ -0,0 +1,73 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# W02 Image Basics\n", + "請先上傳講義指定的輸入素材,再依序執行。CPU 可完成;Colab GPU 非必要。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip -q install Pillow numpy opencv-python-headless matplotlib librosa soundfile moviepy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"W02:觀察像素、格式與 JPEG 品質。CPU 可執行。\"\"\"\n", + "\n", + "from pathlib import Path\n", + "from PIL import Image\n", + "\n", + "\n", + "def main() -> None:\n", + " source = Path(\"input.jpg\")\n", + " output = Path(\"output_w02\")\n", + " output.mkdir(exist_ok=True)\n", + " if not source.exists():\n", + " raise FileNotFoundError(\"請把測試圖片命名為 input.jpg,放在目前資料夾。\")\n", + "\n", + " image = Image.open(source)\n", + " rgb = image.convert(\"RGB\")\n", + " print(f\"格式:{image.format}|模式:{image.mode}|尺寸:{image.size}\")\n", + " print(f\"左上角 RGB:{rgb.getpixel((0, 0))}\")\n", + "\n", + " rgb.save(output / \"image.png\")\n", + " for quality in (95, 70, 40):\n", + " rgb.save(output / f\"image_q{quality}.jpg\", quality=quality, optimize=True)\n", + "\n", + " for path in sorted(output.iterdir()):\n", + " print(f\"{path.name:18s} {path.stat().st_size / 1024:8.1f} KiB\")\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/course-115/colab/W03_Convolution.ipynb b/course-115/colab/W03_Convolution.ipynb new file mode 100644 index 0000000..586d423 --- /dev/null +++ b/course-115/colab/W03_Convolution.ipynb @@ -0,0 +1,89 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# W03 Convolution\n", + "請先上傳講義指定的輸入素材,再依序執行。CPU 可完成;Colab GPU 非必要。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip -q install Pillow numpy opencv-python-headless matplotlib librosa soundfile moviepy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"W03:使用 NumPy 實作灰階 3×3 卷積。\"\"\"\n", + "\n", + "from pathlib import Path\n", + "import numpy as np\n", + "from PIL import Image\n", + "\n", + "\n", + "KERNELS = {\n", + " \"blur\": np.ones((3, 3), dtype=np.float32) / 9,\n", + " \"sharpen\": np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32),\n", + " \"edge\": np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], np.float32),\n", + "}\n", + "\n", + "\n", + "def convolve_gray(image: np.ndarray, kernel: np.ndarray) -> np.ndarray:\n", + " \"\"\"以 edge padding 計算 3×3 卷積,輸出截斷到 0–255。\"\"\"\n", + " padded = np.pad(image.astype(np.float32), 1, mode=\"edge\")\n", + " output = np.zeros_like(image, dtype=np.float32)\n", + " for row in range(image.shape[0]):\n", + " for col in range(image.shape[1]):\n", + " window = padded[row : row + 3, col : col + 3]\n", + " output[row, col] = np.sum(window * kernel)\n", + " return np.clip(output, 0, 255).astype(np.uint8)\n", + "\n", + "\n", + "def main() -> None:\n", + " source = Path(\"input.jpg\")\n", + " output_dir = Path(\"output_w03\")\n", + " output_dir.mkdir(exist_ok=True)\n", + " if not source.exists():\n", + " raise FileNotFoundError(\"請把測試圖片命名為 input.jpg。\")\n", + "\n", + " # 縮小可讓純 Python 迴圈在一般 CPU 上快速完成。\n", + " image = Image.open(source).convert(\"L\")\n", + " image.thumbnail((640, 640))\n", + " gray = np.asarray(image)\n", + " for name, kernel in KERNELS.items():\n", + " result = convolve_gray(gray, kernel)\n", + " Image.fromarray(result).save(output_dir / f\"{name}.png\")\n", + " print(name, kernel.tolist())\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/course-115/colab/W04_Diffusion_Forward.ipynb b/course-115/colab/W04_Diffusion_Forward.ipynb new file mode 100644 index 0000000..ea7d9a4 --- /dev/null +++ b/course-115/colab/W04_Diffusion_Forward.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# W04 Diffusion Forward\n", + "請先上傳講義指定的輸入素材,再依序執行。CPU 可完成;Colab GPU 非必要。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip -q install Pillow numpy opencv-python-headless matplotlib librosa soundfile moviepy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"W04:依 DDPM 的封閉形式模擬前向加噪;不需 GPU 或模型權重。\"\"\"\n", + "\n", + "from pathlib import Path\n", + "import numpy as np\n", + "from PIL import Image, ImageDraw\n", + "\n", + "\n", + "def to_uint8(array: np.ndarray) -> np.ndarray:\n", + " return np.clip(array * 255, 0, 255).astype(np.uint8)\n", + "\n", + "\n", + "def main() -> None:\n", + " source = Path(\"input.jpg\")\n", + " if not source.exists():\n", + " raise FileNotFoundError(\"請把測試圖片命名為 input.jpg。\")\n", + "\n", + " image = Image.open(source).convert(\"RGB\").resize((320, 320))\n", + " x0 = np.asarray(image, dtype=np.float32) / 255.0\n", + " rng = np.random.default_rng(42)\n", + " epsilon = rng.normal(0, 1, x0.shape).astype(np.float32)\n", + " alpha_bars = (1.0, 0.95, 0.70, 0.30, 0.05)\n", + " panels: list[Image.Image] = []\n", + "\n", + " for alpha_bar in alpha_bars:\n", + " xt = np.sqrt(alpha_bar) * x0 + np.sqrt(1 - alpha_bar) * epsilon\n", + " panel = Image.fromarray(to_uint8(xt))\n", + " canvas = Image.new(\"RGB\", (320, 350), \"white\")\n", + " canvas.paste(panel, (0, 30))\n", + " ImageDraw.Draw(canvas).text((8, 8), f\"alpha_bar={alpha_bar:.2f}\", fill=\"black\")\n", + " panels.append(canvas)\n", + "\n", + " montage = Image.new(\"RGB\", (320 * len(panels), 350), \"white\")\n", + " for index, panel in enumerate(panels):\n", + " montage.paste(panel, (index * 320, 0))\n", + " montage.save(\"w04_forward_diffusion.png\")\n", + " print(\"已輸出 w04_forward_diffusion.png;固定 seed=42。\")\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/course-115/colab/W10_Audio_Spectrum.ipynb b/course-115/colab/W10_Audio_Spectrum.ipynb new file mode 100644 index 0000000..36f092f --- /dev/null +++ b/course-115/colab/W10_Audio_Spectrum.ipynb @@ -0,0 +1,78 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# W10 Audio Spectrum\n", + "請先上傳講義指定的輸入素材,再依序執行。CPU 可完成;Colab GPU 非必要。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip -q install Pillow numpy opencv-python-headless matplotlib librosa soundfile moviepy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"W10:繪製波形與 STFT 頻譜。\"\"\"\n", + "\n", + "from pathlib import Path\n", + "import librosa\n", + "import librosa.display\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "\n", + "def main() -> None:\n", + " source = Path(\"input.wav\")\n", + " if not source.exists():\n", + " raise FileNotFoundError(\"請把 3–5 秒 WAV 命名為 input.wav。\")\n", + "\n", + " y, sr = librosa.load(source, sr=None, mono=True)\n", + " n_fft, hop_length = 1024, 256\n", + " stft = librosa.stft(y, n_fft=n_fft, hop_length=hop_length)\n", + " db = librosa.amplitude_to_db(np.abs(stft), ref=np.max)\n", + " print(f\"取樣率:{sr} Hz|樣本數:{len(y)}|秒數:{len(y) / sr:.3f}\")\n", + "\n", + " fig, axes = plt.subplots(2, 1, figsize=(12, 7), constrained_layout=True)\n", + " librosa.display.waveshow(y, sr=sr, ax=axes[0])\n", + " axes[0].set(title=\"Waveform\", xlabel=\"Time (s)\", ylabel=\"Amplitude\")\n", + " image = librosa.display.specshow(\n", + " db, sr=sr, hop_length=hop_length, x_axis=\"time\", y_axis=\"hz\", ax=axes[1]\n", + " )\n", + " axes[1].set(title=f\"STFT|n_fft={n_fft}, hop={hop_length}\")\n", + " fig.colorbar(image, ax=axes[1], format=\"%+2.0f dB\")\n", + " fig.savefig(\"w10_waveform_stft.png\", dpi=160)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/course-115/colab/W11_Audio_Edit.ipynb b/course-115/colab/W11_Audio_Edit.ipynb new file mode 100644 index 0000000..f6731e3 --- /dev/null +++ b/course-115/colab/W11_Audio_Edit.ipynb @@ -0,0 +1,77 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# W11 Audio Edit\n", + "請先上傳講義指定的輸入素材,再依序執行。CPU 可完成;Colab GPU 非必要。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip -q install Pillow numpy opencv-python-headless matplotlib librosa soundfile moviepy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"W11:使用 librosa/soundfile 完成裁切、淡入淡出與峰值正規化。\"\"\"\n", + "\n", + "from pathlib import Path\n", + "import librosa\n", + "import numpy as np\n", + "import soundfile as sf\n", + "\n", + "\n", + "def main() -> None:\n", + " source = Path(\"input.wav\")\n", + " if not source.exists():\n", + " raise FileNotFoundError(\"請把旁白 WAV 命名為 input.wav。\")\n", + "\n", + " y, sr = librosa.load(source, sr=None, mono=True)\n", + " start, end = int(1.0 * sr), min(int(9.0 * sr), len(y))\n", + " clip = y[start:end].copy()\n", + " if clip.size == 0:\n", + " raise ValueError(\"音檔太短,無法取出 1–9 秒範圍。\")\n", + "\n", + " peak = np.max(np.abs(clip))\n", + " if peak > 0:\n", + " clip = 0.9 * clip / peak\n", + "\n", + " fade_samples = min(int(0.15 * sr), len(clip) // 2)\n", + " clip[:fade_samples] *= np.linspace(0, 1, fade_samples)\n", + " clip[-fade_samples:] *= np.linspace(1, 0, fade_samples)\n", + " sf.write(\"w11_narration_edit.wav\", clip, sr, subtype=\"PCM_16\")\n", + " print(f\"輸出 {len(clip) / sr:.2f} 秒|取樣率 {sr} Hz|峰值 {np.max(np.abs(clip)):.3f}\")\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/course-115/colab/W15_Make_Short.ipynb b/course-115/colab/W15_Make_Short.ipynb new file mode 100644 index 0000000..ddf7f6d --- /dev/null +++ b/course-115/colab/W15_Make_Short.ipynb @@ -0,0 +1,88 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# W15 Make Short\n", + "請先上傳講義指定的輸入素材,再依序執行。CPU 可完成;Colab GPU 非必要。\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip -q install Pillow numpy opencv-python-headless matplotlib librosa soundfile moviepy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"W15:用 MoviePy 將圖片、旁白與字幕合成直式短影音。\"\"\"\n", + "\n", + "from pathlib import Path\n", + "from moviepy import AudioFileClip, ImageClip, TextClip, CompositeVideoClip, concatenate_videoclips\n", + "\n", + "\n", + "WIDTH, HEIGHT, FPS = 720, 1280, 30\n", + "\n", + "\n", + "def fit_vertical(path: Path, seconds: float) -> ImageClip:\n", + " clip = ImageClip(str(path)).with_duration(seconds)\n", + " scale = min(WIDTH / clip.w, HEIGHT / clip.h)\n", + " return clip.resized(scale).with_position((\"center\", \"center\"))\n", + "\n", + "\n", + "def main() -> None:\n", + " images = sorted(Path(\"image\").glob(\"*.png\")) + sorted(Path(\"image\").glob(\"*.jpg\"))\n", + " audio_path = Path(\"audio/narration.wav\")\n", + " if not images or not audio_path.exists():\n", + " raise FileNotFoundError(\"需要 image/ 內的圖片,以及 audio/narration.wav。\")\n", + "\n", + " audio = AudioFileClip(str(audio_path))\n", + " seconds = audio.duration / len(images)\n", + " scenes = []\n", + " for index, path in enumerate(images, start=1):\n", + " background = ImageClip(color=(18, 20, 28), size=(WIDTH, HEIGHT)).with_duration(seconds)\n", + " image = fit_vertical(path, seconds)\n", + " label = TextClip(\n", + " text=f\"Scene {index}\", font_size=42, color=\"white\", method=\"caption\", size=(620, None)\n", + " ).with_duration(seconds).with_position((\"center\", 1080))\n", + " scenes.append(CompositeVideoClip([background, image, label], size=(WIDTH, HEIGHT)))\n", + "\n", + " video = concatenate_videoclips(scenes, method=\"compose\").with_audio(audio)\n", + " Path(\"output\").mkdir(exist_ok=True)\n", + " video.write_videofile(\n", + " \"output/rough_cut.mp4\", fps=FPS, codec=\"libx264\", audio_codec=\"aac\", preset=\"medium\"\n", + " )\n", + " video.close()\n", + " audio.close()\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()\n", + "\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/course-115/common-syllabus-2026.md b/course-115/common-syllabus-2026.md new file mode 100644 index 0000000..e113cb2 --- /dev/null +++ b/course-115/common-syllabus-2026.md @@ -0,0 +1,94 @@ +# 多媒體系統 115 學年度共同課綱 + +## 課程定位 + +這門課不是「輪流體驗幾個 AI 網站」,而是從媒體資料的表示方式出發,理解影像、聲音與影片如何被電腦儲存、轉換、生成與組合,最後完成一支可說明技術選擇的短影音作品。 + +課程採一套共同教材,日間部與進修部使用相同核心概念與範例,再以任務支架調整難度: + +- **共同必學**:兩班都要完成的觀念、實驗與安全規範。 +- **日間引導**:縮小變因、提供步驟與檢核點,強化一年級學生的技術基礎。 +- **進修挑戰**:保留核心檢核,但增加自選素材、工作流程整合與專題時間。 + +## 學習成果 + +完成課程後,學生應能: + +1. 以像素、取樣、影格與壓縮等概念說明數位媒體的表示方式。 +2. 使用 Python、Pillow、OpenCV、librosa、MoviePy 或 FFmpeg 完成基本媒體處理。 +3. 說明擴散模型、潛在空間、交叉注意力、引導強度與 LoRA 的角色。 +4. 說明頻譜、STFT、梅爾頻譜、聲紋嵌入與 vocoder 的基本關係。 +5. 分辨容器、編碼器、畫面更新率、關鍵影格、位元率與檔案大小的關係。 +6. 在一般 8 GB RAM、CPU 電腦與免費 Colab 的限制下,選擇可重現的工作流程。 +7. 使用生成式 AI 協助構想、素材製作與程式除錯,同時保留提示、參數、來源與驗證紀錄。 +8. 遵守同意、肖像、聲音、著作權與 AI 生成內容揭露規範。 + +## 18 週課程地圖 + +| 週次 | 核心問題 | 技術主題 | 課堂產出 | +|---:|---|---|---| +| W01 | 什麼才算一個多媒體系統? | 媒體管線、Markdown、HackMD、素材與 AI 紀錄 | 個人課程筆記與媒體拆解圖 | +| W02 | 一張圖片在電腦裡是什麼? | 像素、RGB/RGBA、解析度、色彩深度、格式與壓縮 | 圖片參數實驗 | +| W03 | 濾鏡為什麼能找出邊緣? | Pillow、NumPy、OpenCV、卷積核、模糊與銳化 | CPU 影像濾鏡實驗 | +| W04 | AI 如何從雜訊生成圖片? | DDPM、前向加噪、反向去噪、U-Net、時間步 | 擴散過程模擬與概念卡 | +| W05 | 文字如何控制生成影像? | VAE、潛在擴散、文字編碼、交叉注意力、CFG、seed | 可重現的提示實驗 | +| W06 | 影像微調到底改了什麼? | 資料集、過擬合、DreamBooth 概念、LoRA 低秩更新 | 微調企畫與資料卡 | +| W07 | 如何做出一致而可信的影像系列? | 風格規格、構圖、生成後處理、版本與來源 | 3–6 張影像故事組 | +| W08 | 我能不能解釋作品背後的技術? | 影像模組統整、口頭問答、期中展示 | 期中影像作品與技術說明 | +| W09 | 期中考週 | **不授新課**;依校務規定進行評量 | 無新作業 | +| W10 | 聲音如何變成可以運算的數字? | 取樣率、位元深度、Nyquist、波形、FFT、STFT | 聲音與頻譜觀察 | +| W11 | 剪輯聲音時實際改變了什麼? | 裁切、淡入淡出、正規化、濾波、變速與變調 | 聲音重製練習 | +| W12 | 聲音深偽如何生成,又如何辨識風險? | TTS、梅爾頻譜、vocoder、speaker embedding、voice conversion | 合成/真實聲音比較與揭露卡 | +| W13 | 短影音不是把素材排在一起而已嗎? | 分鏡、時間軸、FPS、畫面比例、敘事節奏、字幕設計 | 15–30 秒分鏡與腳本 | +| W14 | 如何建立可追溯的多媒體素材庫? | 圖像、旁白、音效、字幕、metadata、provenance | 專題素材包 | +| W15 | 影片如何被合成與壓縮? | FFmpeg、MoviePy、container、codec、bitrate、keyframe/GOP | 720×1280 粗剪版 | +| W16 | 作品是否已經成為一個系統? | 管線整合、錯誤處理、測試、進度小報告 | Rough cut、架構圖與風險清單 | +| W17 | 如何讓觀眾相信我的技術說明? | 期末發表 A、個人口頭問答與同儕回饋 | 完整作品與答辯 | +| W18 | 如何從成果回推可改進的系統? | 期末發表 B、互評、反思與封存 | 完整作品、README、AI 使用紀錄 | + +## 課程設備策略 + +所有必做內容都必須具備 **CPU 基準路徑**。需要較多算力的生成或微調,採下列順序: + +1. 先用紙上模型、少量 NumPy 程式或預先產生的中間結果理解原理。 +2. 再使用 ChatGPT/Gemini 免費版完成少量生成實驗。 +3. 免費 Colab GPU 只作為可用時的加速選項,不把 GPU 配額當作作業前提。 +4. LoRA 以資料規劃、參數影響與成品比較為必做;實際訓練屬加分或教師示範。 + +## 評量建議 + +### 日間部 + +- 每週概念檢核與 Exit Ticket:20% +- 引導式實作與媒體實驗:25% +- W08 期中影像作品:20% +- W16 進度小報告:10% +- W17–18 期末短影音與答辯:25% + +### 進修部 + +- 共同概念檢核與課堂參與:15% +- 階段作品與技術紀錄:20% +- W08 期中影像作品:20% +- W16 進度小報告:10% +- W17–18 期末短影音與答辯:35% + +## 期末作品最低規格 + +- 直式 720×1280、15–30 秒、MP4。 +- 3–6 個視覺素材,具旁白與字幕;背景音樂至多一軌。 +- 至少使用一項本課程的影像處理或生成技術,以及一項聲音處理技術。 +- 繳交成品、來源素材、程式或可重現步驟、`README.md`、`AI_USAGE.md` 與素材授權/同意紀錄。 +- 能個別說明輸入、輸出、關鍵參數、失敗案例、限制與改善方法。 + +## 深偽與生成內容安全界線 + +- 僅能使用自己的聲音/影像、取得明確同意的素材,或完全合成的角色。 +- 禁止仿冒同學、教師、公眾人物或未同意者;禁止製作詐騙、羞辱、色情或可能造成誤認的內容。 +- 所有生成或合成內容都必須清楚標示,並保留來源、同意、提示、工具與後製紀錄。 +- 「能生成」不等於「可以發布」。公開前必須重新確認授權與可識別個資。 + +## 共用教學節奏 + +每週建議以「情境問題 → 原理模型 → 小型實驗 → 創作任務 → 口頭說明」進行。技術不只留在投影片:學生必須改變一個參數、觀察一個結果、提出一個解釋,並留下可以重現的紀錄。 + diff --git a/course-115/hackmd.book.md b/course-115/hackmd.book.md new file mode 100644 index 0000000..da820f8 --- /dev/null +++ b/course-115/hackmd.book.md @@ -0,0 +1,22 @@ +# 課程教材清單 + +## weeks + +- [W01|先拆解,再創作:多媒體系統與 HackMD](https://hackmd.io/@wiimax/SJG50wvLzl) +- [W02|一張圖片在電腦裡是什麼?](https://hackmd.io/@wiimax/HJNm5CvwLzx) +- [W03|濾鏡為什麼能找出邊緣?](https://hackmd.io/@wiimax/S1V45CwPIfg) +- [W04|AI 如何從雜訊生成圖片?](https://hackmd.io/@wiimax/SJHqRwvIMx) +- [W05|文字如何控制生成影像?](https://hackmd.io/@wiimax/SkUqAPv8Me) +- [W06|影像微調到底改了什麼?](https://hackmd.io/@wiimax/BkHL9RPv8Ge) +- [W07|生成影像工作室:一致性比單張漂亮更重要](https://hackmd.io/@wiimax/r1wcCwwUfl) +- [W08|期中影像展:把「我覺得好看」變成可說明的技術判斷](https://hackmd.io/@wiimax/r1uqCvwIfx) +- [W09|期中考週:不授新課](https://hackmd.io/@wiimax/HyFcRPvLzx) +- [W10|聲音如何變成可以運算的數字?](https://hackmd.io/@wiimax/S1qcRvP8Mg) +- [W11|剪輯聲音時,實際改變了什麼?](https://hackmd.io/@wiimax/ry59CDwUGg) +- [W12|聲音深偽如何生成,又如何辨識風險?](https://hackmd.io/@wiimax/SyEj9AwPUfx) +- [W13|短影音不是把素材排在一起而已嗎?](https://hackmd.io/@wiimax/Bk39CDwUMg) +- [W14|如何建立可追溯的多媒體素材庫?](https://hackmd.io/@wiimax/Bk2qADw8fg) +- [W15|影片如何被合成與壓縮?](https://hackmd.io/@wiimax/BkEpq0wPUGx) +- [W16|進度小報告:作品是否已經成為一個系統?](https://hackmd.io/@wiimax/SJRqCPDIzl) +- [W17|期末成果發表 A](https://hackmd.io/@wiimax/Sy09APPUzx) +- [W18|期末成果發表 B:把作品留下來,也把判斷留下來](https://hackmd.io/@wiimax/BkkiCwwLfe) diff --git a/course-115/labs/requirements.txt b/course-115/labs/requirements.txt new file mode 100644 index 0000000..6646de3 --- /dev/null +++ b/course-115/labs/requirements.txt @@ -0,0 +1,8 @@ +Pillow>=10,<12 +numpy>=1.26,<3 +opencv-python-headless>=4.9,<5 +matplotlib>=3.8,<4 +librosa>=0.10,<1 +soundfile>=0.12,<1 +moviepy>=2,<3 + diff --git a/course-115/labs/w02_image_basics.py b/course-115/labs/w02_image_basics.py new file mode 100644 index 0000000..9d16529 --- /dev/null +++ b/course-115/labs/w02_image_basics.py @@ -0,0 +1,29 @@ +"""W02:觀察像素、格式與 JPEG 品質。CPU 可執行。""" + +from pathlib import Path +from PIL import Image + + +def main() -> None: + source = Path("input.jpg") + output = Path("output_w02") + output.mkdir(exist_ok=True) + if not source.exists(): + raise FileNotFoundError("請把測試圖片命名為 input.jpg,放在目前資料夾。") + + image = Image.open(source) + rgb = image.convert("RGB") + print(f"格式:{image.format}|模式:{image.mode}|尺寸:{image.size}") + print(f"左上角 RGB:{rgb.getpixel((0, 0))}") + + rgb.save(output / "image.png") + for quality in (95, 70, 40): + rgb.save(output / f"image_q{quality}.jpg", quality=quality, optimize=True) + + for path in sorted(output.iterdir()): + print(f"{path.name:18s} {path.stat().st_size / 1024:8.1f} KiB") + + +if __name__ == "__main__": + main() + diff --git a/course-115/labs/w03_convolution.py b/course-115/labs/w03_convolution.py new file mode 100644 index 0000000..4434a78 --- /dev/null +++ b/course-115/labs/w03_convolution.py @@ -0,0 +1,45 @@ +"""W03:使用 NumPy 實作灰階 3×3 卷積。""" + +from pathlib import Path +import numpy as np +from PIL import Image + + +KERNELS = { + "blur": np.ones((3, 3), dtype=np.float32) / 9, + "sharpen": np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32), + "edge": np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], np.float32), +} + + +def convolve_gray(image: np.ndarray, kernel: np.ndarray) -> np.ndarray: + """以 edge padding 計算 3×3 卷積,輸出截斷到 0–255。""" + padded = np.pad(image.astype(np.float32), 1, mode="edge") + output = np.zeros_like(image, dtype=np.float32) + for row in range(image.shape[0]): + for col in range(image.shape[1]): + window = padded[row : row + 3, col : col + 3] + output[row, col] = np.sum(window * kernel) + return np.clip(output, 0, 255).astype(np.uint8) + + +def main() -> None: + source = Path("input.jpg") + output_dir = Path("output_w03") + output_dir.mkdir(exist_ok=True) + if not source.exists(): + raise FileNotFoundError("請把測試圖片命名為 input.jpg。") + + # 縮小可讓純 Python 迴圈在一般 CPU 上快速完成。 + image = Image.open(source).convert("L") + image.thumbnail((640, 640)) + gray = np.asarray(image) + for name, kernel in KERNELS.items(): + result = convolve_gray(gray, kernel) + Image.fromarray(result).save(output_dir / f"{name}.png") + print(name, kernel.tolist()) + + +if __name__ == "__main__": + main() + diff --git a/course-115/labs/w04_diffusion_forward.py b/course-115/labs/w04_diffusion_forward.py new file mode 100644 index 0000000..6043b33 --- /dev/null +++ b/course-115/labs/w04_diffusion_forward.py @@ -0,0 +1,41 @@ +"""W04:依 DDPM 的封閉形式模擬前向加噪;不需 GPU 或模型權重。""" + +from pathlib import Path +import numpy as np +from PIL import Image, ImageDraw + + +def to_uint8(array: np.ndarray) -> np.ndarray: + return np.clip(array * 255, 0, 255).astype(np.uint8) + + +def main() -> None: + source = Path("input.jpg") + if not source.exists(): + raise FileNotFoundError("請把測試圖片命名為 input.jpg。") + + image = Image.open(source).convert("RGB").resize((320, 320)) + x0 = np.asarray(image, dtype=np.float32) / 255.0 + rng = np.random.default_rng(42) + epsilon = rng.normal(0, 1, x0.shape).astype(np.float32) + alpha_bars = (1.0, 0.95, 0.70, 0.30, 0.05) + panels: list[Image.Image] = [] + + for alpha_bar in alpha_bars: + xt = np.sqrt(alpha_bar) * x0 + np.sqrt(1 - alpha_bar) * epsilon + panel = Image.fromarray(to_uint8(xt)) + canvas = Image.new("RGB", (320, 350), "white") + canvas.paste(panel, (0, 30)) + ImageDraw.Draw(canvas).text((8, 8), f"alpha_bar={alpha_bar:.2f}", fill="black") + panels.append(canvas) + + montage = Image.new("RGB", (320 * len(panels), 350), "white") + for index, panel in enumerate(panels): + montage.paste(panel, (index * 320, 0)) + montage.save("w04_forward_diffusion.png") + print("已輸出 w04_forward_diffusion.png;固定 seed=42。") + + +if __name__ == "__main__": + main() + diff --git a/course-115/labs/w10_audio_spectrum.py b/course-115/labs/w10_audio_spectrum.py new file mode 100644 index 0000000..e3fb031 --- /dev/null +++ b/course-115/labs/w10_audio_spectrum.py @@ -0,0 +1,34 @@ +"""W10:繪製波形與 STFT 頻譜。""" + +from pathlib import Path +import librosa +import librosa.display +import matplotlib.pyplot as plt +import numpy as np + + +def main() -> None: + source = Path("input.wav") + if not source.exists(): + raise FileNotFoundError("請把 3–5 秒 WAV 命名為 input.wav。") + + y, sr = librosa.load(source, sr=None, mono=True) + n_fft, hop_length = 1024, 256 + stft = librosa.stft(y, n_fft=n_fft, hop_length=hop_length) + db = librosa.amplitude_to_db(np.abs(stft), ref=np.max) + print(f"取樣率:{sr} Hz|樣本數:{len(y)}|秒數:{len(y) / sr:.3f}") + + fig, axes = plt.subplots(2, 1, figsize=(12, 7), constrained_layout=True) + librosa.display.waveshow(y, sr=sr, ax=axes[0]) + axes[0].set(title="Waveform", xlabel="Time (s)", ylabel="Amplitude") + image = librosa.display.specshow( + db, sr=sr, hop_length=hop_length, x_axis="time", y_axis="hz", ax=axes[1] + ) + axes[1].set(title=f"STFT|n_fft={n_fft}, hop={hop_length}") + fig.colorbar(image, ax=axes[1], format="%+2.0f dB") + fig.savefig("w10_waveform_stft.png", dpi=160) + + +if __name__ == "__main__": + main() + diff --git a/course-115/labs/w11_audio_edit.py b/course-115/labs/w11_audio_edit.py new file mode 100644 index 0000000..548e72d --- /dev/null +++ b/course-115/labs/w11_audio_edit.py @@ -0,0 +1,33 @@ +"""W11:使用 librosa/soundfile 完成裁切、淡入淡出與峰值正規化。""" + +from pathlib import Path +import librosa +import numpy as np +import soundfile as sf + + +def main() -> None: + source = Path("input.wav") + if not source.exists(): + raise FileNotFoundError("請把旁白 WAV 命名為 input.wav。") + + y, sr = librosa.load(source, sr=None, mono=True) + start, end = int(1.0 * sr), min(int(9.0 * sr), len(y)) + clip = y[start:end].copy() + if clip.size == 0: + raise ValueError("音檔太短,無法取出 1–9 秒範圍。") + + peak = np.max(np.abs(clip)) + if peak > 0: + clip = 0.9 * clip / peak + + fade_samples = min(int(0.15 * sr), len(clip) // 2) + clip[:fade_samples] *= np.linspace(0, 1, fade_samples) + clip[-fade_samples:] *= np.linspace(1, 0, fade_samples) + sf.write("w11_narration_edit.wav", clip, sr, subtype="PCM_16") + print(f"輸出 {len(clip) / sr:.2f} 秒|取樣率 {sr} Hz|峰值 {np.max(np.abs(clip)):.3f}") + + +if __name__ == "__main__": + main() + diff --git a/course-115/labs/w15_make_short.py b/course-115/labs/w15_make_short.py new file mode 100644 index 0000000..6554424 --- /dev/null +++ b/course-115/labs/w15_make_short.py @@ -0,0 +1,44 @@ +"""W15:用 MoviePy 將圖片、旁白與字幕合成直式短影音。""" + +from pathlib import Path +from moviepy import AudioFileClip, ImageClip, TextClip, CompositeVideoClip, concatenate_videoclips + + +WIDTH, HEIGHT, FPS = 720, 1280, 30 + + +def fit_vertical(path: Path, seconds: float) -> ImageClip: + clip = ImageClip(str(path)).with_duration(seconds) + scale = min(WIDTH / clip.w, HEIGHT / clip.h) + return clip.resized(scale).with_position(("center", "center")) + + +def main() -> None: + images = sorted(Path("image").glob("*.png")) + sorted(Path("image").glob("*.jpg")) + audio_path = Path("audio/narration.wav") + if not images or not audio_path.exists(): + raise FileNotFoundError("需要 image/ 內的圖片,以及 audio/narration.wav。") + + audio = AudioFileClip(str(audio_path)) + seconds = audio.duration / len(images) + scenes = [] + for index, path in enumerate(images, start=1): + background = ImageClip(color=(18, 20, 28), size=(WIDTH, HEIGHT)).with_duration(seconds) + image = fit_vertical(path, seconds) + label = TextClip( + text=f"Scene {index}", font_size=42, color="white", method="caption", size=(620, None) + ).with_duration(seconds).with_position(("center", 1080)) + scenes.append(CompositeVideoClip([background, image, label], size=(WIDTH, HEIGHT))) + + video = concatenate_videoclips(scenes, method="compose").with_audio(audio) + Path("output").mkdir(exist_ok=True) + video.write_videofile( + "output/rough_cut.mp4", fps=FPS, codec="libx264", audio_codec="aac", preset="medium" + ) + video.close() + audio.close() + + +if __name__ == "__main__": + main() + diff --git a/course-115/notebooklm/NotebookLM_Course_Overview.pptx b/course-115/notebooklm/NotebookLM_Course_Overview.pptx new file mode 100644 index 0000000..19978f2 Binary files /dev/null and b/course-115/notebooklm/NotebookLM_Course_Overview.pptx differ diff --git a/course-115/notebooklm/NotebookLM_Study_Guide.md b/course-115/notebooklm/NotebookLM_Study_Guide.md new file mode 100644 index 0000000..66559ff --- /dev/null +++ b/course-115/notebooklm/NotebookLM_Study_Guide.md @@ -0,0 +1,182 @@ +# 多媒體系統課程學習指南 + +本學習指南專為大專一年級學生設計,旨在系統化梳理從數位影像、生成式 AI、聲音處理到短影音合成的核心知識。本課程強調技術原理與實作驗證並重,協助學生建立可重現、可說明的多媒體管線。 + +--- + +## 模組一:系統基礎與影像原理 (W01–W03) + +### W01|多媒體系統與 HackMD +* **核心概念**: + * **多媒體系統四問**:輸入(來源格式)、處理(濾鏡/生成/壓縮)、輸出(對象/平台)、驗證(品質/執行時間/授權)。 + * **Markdown 與圖片替代文字**:使用 Markdown 進行知識保存與版本控管;替代文字(Alt text)確保可及性。 +* **常見誤解**:認為多媒體系統只是「會剪片」或「會下 AI 指令」,忽略了系統整體的管線(Pipeline)與驗證機制。 +* **CPU 驗證問題**:如何透過系統圖(最小系統圖)預測處理流程中哪個節點最容易出錯? +* **口頭問答**:多媒體「檔案」與「系統」有何不同? + +### W02|數位影像與像素 +* **核心概念**: + * **像素與解析度**:影像由規則格點組成,解析度減半(寬、高各除以 2)時,總像素會變為原本的 1/4。 + * **顏色模式**:L(灰階)、RGB(紅綠藍)、RGBA(含透明度通道)。 + * **壓縮格式**:PNG(無失真)、JPEG(有失真)、WebP(支援多樣性)。 +* **常見誤解**:解析度越高代表畫面一定越清楚(忽略了鏡頭品質與壓縮影響)。 +* **CPU 驗證問題**:使用 Python Pillow 讀取影像,若將同一張圖輸出為品質 95 與 40 的 JPEG,檔案大小與邊緣漸層有何具體變化? +* **口頭問答**:若要將透明圖示放進影片,為何不選 JPEG? + +### W03|卷積與影像處理 +* **核心概念**: + * **卷積核(Kernel)**:小矩陣在影像上移動,透過加權相加產生新像素。 + * **濾鏡效果**:模糊(鄰域平均)、銳化(強調中心差異)、邊緣偵測(捕捉亮度快速變化)。 + * **邊界處理與截斷**:處理數值超出 0–255 範圍或影像邊緣缺失像素的問題。 +* **常見誤解**:邊緣偵測是在辨識物體(實際是在找局部像素的變化)。 +* **CPU 驗證問題**:為什麼平均核(Average Kernel)在計算後通常要除以權重總和? +* **口頭問答**:若卷積處理後結果整張發黑,最先檢查哪兩件事?(提示:資料型別與數值範圍)。 + +--- + +## 模組二:擴散模型與生成式影像 (W04–W07) + +### W04|擴散模型原理 (DDPM) +* **核心概念**: + * **前向加噪與反向去噪**:前向過程逐步加入高斯雜訊;模型學習的是在特定時間步(Time-step)下預測並移除雜訊。 + * **U-Net 架構**:負責擷取特徵並恢復空間資訊。 +* **常見誤解**:AI 是從資料夾拼貼訓練圖(實際是學會參數化的統計關係)。 +* **CPU 驗證問題**:前向加噪過程中,當 $\alphā_t$ 從 0.95 降至 0.05 時,影像訊號比例如何改變? +* **口頭問答**:擴散模型生成時,模型預測的是最終圖片還是每一步要移除的成分? + +### W05|文字控制與潛在擴散 (LDM) +* **核心概念**: + * **VAE(自編碼器)**:將圖片壓縮至潛在空間(Latent Space),大幅降低運算成本。 + * **控制變因**:提示文字(Prompt)、Seed(隨機狀態)、Steps(取樣步數)、CFG(引導強度)。 +* **常見誤解**:CFG(Guidance)越高越好(過高會導致過度飽和或構圖僵硬)。 +* **CPU 驗證問題**:固定 Seed 的情況下,僅改變提示中的「環境描述」,畫面的一致性會發生什麼變化? +* **口頭問答**:固定 Seed 能控制什麼,不能保證什麼? + +### W06|影像微調 (LoRA) +* **核心概念**: + * **LoRA(低秩更新)**:不改變原模型權重,僅訓練極小的額外矩陣(秩 $r$ 很小),以低成本學習特定概念。 + * **資料集品質**:標註(Caption)、多樣性與授權是微調成功的關鍵。 +* **常見誤解**:LoRA 檔案小代表權利風險低(風險取決於訓練資料來源)。 +* **CPU 驗證問題**:過擬合(Overfitting)時,模型對未參與訓練的提示語會產生什麼反應? +* **口頭問答**:如何用「未參與訓練的提示」判斷模型的泛化能力? + +### W07|一致性影像創作 +* **核心概念**: + * **視覺規格表**:定義角色特徵、色盤、構圖語言與媒材風格。 + * **變因管理**:區分固定變因(如比例、主體)與每張改變的變因(如動作)。 +* **常見誤解**:AI 輸出就是成品(實際上仍需透過後製如裁切、補邊、色調調整來達成統一)。 +* **CPU 驗證問題**:如何使用 Pillow 統一不同生成結果的尺寸且不拉伸人物比例? +* **口頭問答**:哪一項一致性(角色、光線、背景)最難單靠提示控制? + +--- + +## 模組三:期中回顧與休息 (W08–W09) + +### W08|影像展與技術答辯 +* **核心重點**:將「我覺得好看」轉化為技術判斷,說明格式、參數、失敗改善與技術限制。 +* **口頭問答練習**: + * 解析度與檔案大小為何不一定成正比? + * 潛在擴散模型為何比像素級擴散更省計算資源? + +### W09|期中考週 +* **不授新課**:此週用於整理 W01–W08 筆記,檢查檔案紀錄完整性。 + +--- + +## 模組四:聲音訊號與深偽辨識 (W10–W12) + +### W10|聲音數位化與頻譜 +* **核心概念**: + * **取樣參數**:取樣率(如 44.1 kHz)、位元深度(振幅解析度)。 + * **Nyquist 定理**:取樣率需高於最高頻率的兩倍以避免混疊。 + * **時頻分析**:波形(時間—振幅)、FFT(頻率分布)、STFT(時間—頻率分布)。 +* **常見誤解**:44.1 kHz 取樣率代表人類能聽到 44.1 kHz 的聲音(實際上是為了完整記錄 20 kHz 以內的聲音)。 +* **CPU 驗證問題**:當 STFT 的窗長(n_fft)增加時,時間解析度與頻率解析度如何取捨? +* **口頭問答**:為什麼觀察拍手聲適合看波形,而觀察母音適合看頻譜? + +### W11|聲音剪輯與處理 +* **核心概念**: + * **基礎操作**:裁切、淡入/淡出(避免喀聲)、正規化(Normalization)。 + * **進階處理**:動態壓縮、濾波降噪、變速變調(Time stretch/Pitch shift)。 +* **常見誤解**:正規化等於動態壓縮(正規化只調整整體增益,不改變動態範圍)。 +* **CPU 驗證問題**:直接加快播放速度(不使用相位聲碼器)會對音高產生什麼影響? +* **口頭問答**:若錄音時環境噪音過大導致降噪後出現「金屬感」,應如何改善? + +### W12|語音合成 (TTS) 與深偽查證 +* **核心概念**: + * **TTS 管線**:文字 $\rightarrow$ 梅爾頻譜(Mel Spectrogram) $\rightarrow$ 聲碼器(Vocoder) $\rightarrow$ 波形。 + * **聲紋嵌入(Speaker Embedding)**:將聲音特徵向量化以控制音色。 + * **安全規範**:所有合成內容必須標示並取得授權。 +* **常見誤解**:單靠聽覺就能完美辨識深偽(現代模型已能模擬呼吸與韻律)。 +* **CPU 驗證問題**:如何從頻譜圖中尋找聲音合成或轉碼的痕跡? +* **口頭問答**:為什麼不能單憑深偽偵測器的分數就判定一段音訊的真實性? + +--- + +## 模組五:短影音系統合成 (W13–W16) + +### W13|分鏡與時間預算 +* **核心概念**: + * **影片參數**:FPS(影格率)、解析度(720x1280)、Timebase(時間基準)。 + * **腳本結構**:Hook(開場)、Context(情境)、Process(過程)、Result(結果)、Close(結尾)。 +* **常見誤解**:15 秒短片隨便排就好(實際需要精確的旁白計時與字幕安全區規劃)。 +* **CPU 驗證問題**:若 30 FPS 的 20 秒影片需要增加動作流暢度,會改變內容總量還是時間取樣密度? +* **口頭問答**:若生成素材長度不足,應縮小訊息還是增加生成量?理由為何? + +### W14|素材庫與可追溯性 +* **核心概念**: + * **Metadata 與 Provenance**:記錄素材的來源、處理過程、授權紀錄與 AI 揭露資訊。 + * **素材包結構**:區分影像、聲音、腳本與日誌(Asset Log)。 +* **常見誤解**:檔案名稱就是最好的分類方式(應依賴外部 Log 紀錄 id 與來源)。 +* **CPU 驗證問題**:如何透過批次處理腳本檢查素材包中是否有缺檔或比例錯誤的影像? +* **口頭問答**:為什麼在多媒體系統中,原始素材不可以直接被覆寫? + +### W15|影片編碼與合成 +* **核心概念**: + * **容器(Container)與編碼(Codec)**:MP4 是容器,H.264 是編碼;兩者不可混為一談。 + * **冗餘與影格類型**:空間/時間冗餘;I-frame(關鍵影格)、P/B-frame(預測影格)。 + * **FFmpeg/MoviePy**:用於合成、補邊(Pad)與轉碼。 +* **常見誤解**:關鍵影格越多越好(會增加檔案大小)。 +* **CPU 驗證問題**:為什麼不能直接把 4:3 圖片拉伸成 9:16?(提示:比例畸變)。 +* **口頭問答**:H.264 編碼如何利用「時間冗餘」來減小影片體積? + +### W16|系統整合與測試 +* **核心概念**: + * **粗剪版(Rough cut)**:確認聲畫同步與基本邏輯。 + * **三類測試**:正常測試、邊界測試(旁白過長)、錯誤測試(缺檔)。 +* **口頭問答**:若雲端生成服務突然斷線,你的系統有何降級/替代路徑? + +--- + +## 簡答練習題 + +1. **解析度計算**:若一張 1080p 的圖片要縮小為原本面積的 1/4,新的寬與高分別是多少? +2. **卷積運算**:平均核(Mean Filter)的主要功能是什麼?為什麼權重總和需為 1? +3. **擴散模型**:在 DDPM 中,U-Net 網路在「反向過程」中扮演什麼角色? +4. **聲音原理**:Nyquist 頻率與取樣率的關係為何?若要錄製 20,000 Hz 的聲音,取樣率至少要設定為多少? +5. **影片壓縮**:請列舉三種影格類型(I、P、B),並說明哪一種可以獨立解碼? + +--- + +## 申論思考題 + +1. **AI 倫理與責任**:在製作短影音時,若使用了生成式 AI 產生的聲音或影像,你認為最關鍵的「揭露義務」應包含哪些內容?如何避免觀眾誤認? +2. **系統限制下的設計**:若你的運算設備只有 CPU 且記憶體有限,你會如何在「生成品質」與「處理速度」之間取得平衡?請以本學期的實驗經驗說明。 +3. **一致性挑戰**:在多媒體系統中,維持影像角色一致性與聲音音色一致性,哪一個挑戰較大?請從技術原理(如 Seed、Speaker Embedding)的角度分析。 + +--- + +## 重要術語表 (Glossary) + +| 術語 | 說明 | +| :--- | :--- | +| **RGB/RGBA** | 數位影像顏色表示法,A 代表 Alpha 透明度通道。 | +| **Convolution** | 卷積,透過小矩陣運算來提取影像特徵或處理效果。 | +| **Latent Space** | 潛在空間,影像經由 VAE 壓縮後的低維度表示,用於加速擴散過程。 | +| **CFG (Guidance)** | 引導強度,決定生成結果在多大程度上遵循提示文字。 | +| **LoRA** | 低秩適應,一種高效微調模型技術,僅更新少數參數。 | +| **STFT** | 短時傅立葉轉換,將聲音訊號轉化為時間與頻率同時存在的表示法。 | +| **Vocoder** | 聲碼器,將梅爾頻譜等特徵訊號還原成可聽見的波形。 | +| **Container/Codec** | 影片容器(如 MP4)與編碼格式(如 H.264)的區分。 | +| **GOP** | 一組影格結構,決定了關鍵影格與預測影格的排列順序。 | +| **Provenance** | 出處/溯源,記錄數位素材從原始狀態到成品的演變過程。 | \ No newline at end of file diff --git a/course-115/notebooklm/README.md b/course-115/notebooklm/README.md new file mode 100644 index 0000000..62dc209 --- /dev/null +++ b/course-115/notebooklm/README.md @@ -0,0 +1,24 @@ +# NotebookLM 課程來源與產物 + +## 筆記本 + +- 名稱:多媒體系統115|影像、聲音與短影音 +- Notebook ID:`e7ba64b8-3945-4e47-95bd-b3d8c5e90388` +- 建立日期:2026-08-10 + +`source-manifest.json` 保存實際加入的來源與狀態。W01–W05、W08、共同課綱與參考來源為個別來源;因 Google 檔案註冊服務在 W06、W07 連續逾時,其餘週次改為每兩週一份的純文字來源。內容仍取自同一批本機 Markdown,未額外補造資料。 + +## 官方 NotebookLM 產物 + +- `NotebookLM_Study_Guide.md`:繁體中文 study guide,依課程來源生成。 +- `NotebookLM_Course_Overview.pptx`:NotebookLM 原生 21 頁全學期總覽簡報。 +- `artifacts.json`:產物 ID、類型與狀態。 + +NotebookLM 原生簡報是課程級導讀與來源交叉檢查素材;每週正式授課使用 `../slides/` 中由 PPT 技能產生並完成視覺檢查的 W01–W18 簡報。 + +## 使用提醒 + +- NotebookLM 產物可能使用與本課程不同的版面或中英混合標籤,教師授課前仍應快速檢視。 +- 原生產物右下角可能保留 NotebookLM 服務標記,不應移除或冒充自行繪製。 +- 每週概念、公式、作業規格與安全界線,以 `../weeks/` 與 `../common-syllabus-2026.md` 為準。 + diff --git a/course-115/notebooklm/artifacts.json b/course-115/notebooklm/artifacts.json new file mode 100644 index 0000000..628fe60 --- /dev/null +++ b/course-115/notebooklm/artifacts.json @@ -0,0 +1,27 @@ +{ + "notebook_id": "e7ba64b8-3945-4e47-95bd-b3d8c5e90388", + "notebook_title": "多媒體系統115|影像、聲音與短影音", + "artifacts": [ + { + "index": 1, + "id": "e7b8acb7-526b-4cac-ae2e-f1083fd9b185", + "title": "Multimedia Systems Engineering", + "type": "Slide Deck", + "type_id": "slide_deck", + "status": "completed", + "status_id": 3, + "created_at": "2026-08-10T15:17:46+00:00" + }, + { + "index": 2, + "id": "93acf953-f32d-4196-aca6-12bf4ecad8c1", + "title": "多媒體系統課程學習指南", + "type": "Report", + "type_id": "report", + "status": "completed", + "status_id": 3, + "created_at": "2026-08-10T15:16:21+00:00" + } + ], + "count": 2 +} diff --git a/course-115/notebooklm/source-manifest.json b/course-115/notebooklm/source-manifest.json new file mode 100644 index 0000000..6787235 --- /dev/null +++ b/course-115/notebooklm/source-manifest.json @@ -0,0 +1,147 @@ +{ + "notebook_id": "e7ba64b8-3945-4e47-95bd-b3d8c5e90388", + "notebook_title": "多媒體系統115|影像、聲音與短影音", + "sources": [ + { + "index": 1, + "id": "a65001bb-2997-4418-b397-54c8e9725c47", + "title": "W01_課程啟動與多媒體系統", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:10:38+00:00" + }, + { + "index": 2, + "id": "13f0fbea-10e9-46eb-aa08-0839b1c21307", + "title": "W02_數位影像與像素.md", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:10:49+00:00" + }, + { + "index": 3, + "id": "e284694b-4f74-4cda-a8ae-4177645fc2ab", + "title": "W03_卷積與影像處理.md", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:11:00+00:00" + }, + { + "index": 4, + "id": "0f728cb0-59d9-4975-8342-c5e10c0655e3", + "title": "W04_擴散模型原理.md", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:11:12+00:00" + }, + { + "index": 5, + "id": "5a41f935-4112-4509-b001-856f761c1c62", + "title": "W05_潛在擴散與提示控制", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:11:23+00:00" + }, + { + "index": 6, + "id": "c5fc6436-aff4-42fd-b670-50bab026e3e4", + "title": "W08_期中影像展與技術答辯", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:13:59+00:00" + }, + { + "index": 7, + "id": "6ef96a6d-e079-4b0e-90ba-1b9b6b951e84", + "title": "common-syllabus-2026", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:09:37+00:00" + }, + { + "index": 8, + "id": "017a941b-9973-4ff1-ae51-f32b7b6eca12", + "title": "references.md", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:09:49+00:00" + }, + { + "index": 9, + "id": "1d7cd929-f7a2-4ca7-820c-e255eefeb9b2", + "title": "課程講義補充|W06–W07", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:14:53+00:00" + }, + { + "index": 10, + "id": "2db14730-869f-4bf0-b7b5-d82745ab84bd", + "title": "課程講義補充|W09–W10", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:15:01+00:00" + }, + { + "index": 11, + "id": "10217a2f-8a54-4f4e-b104-a229264d9650", + "title": "課程講義補充|W11–W12", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:15:09+00:00" + }, + { + "index": 12, + "id": "0c1f5327-1fa6-4b85-ac3d-dfcad596d6a0", + "title": "課程講義補充|W13–W14", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:15:16+00:00" + }, + { + "index": 13, + "id": "e72f20db-bdd6-4796-847a-bcc1f987989a", + "title": "課程講義補充|W15–W16", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:15:23+00:00" + }, + { + "index": 14, + "id": "f573242e-d065-480d-8701-39b1525c7178", + "title": "課程講義補充|W17–W18", + "type": "markdown", + "url": null, + "status": "ready", + "status_id": 2, + "created_at": "2026-08-10T15:15:30+00:00" + } + ], + "count": 14 +} diff --git a/course-115/references.md b/course-115/references.md new file mode 100644 index 0000000..bdc9045 --- /dev/null +++ b/course-115/references.md @@ -0,0 +1,30 @@ +# 課程主要來源 + +本表優先收錄論文與官方文件,供每週講義、NotebookLM 與簡報共同引用。實際服務介面、免費額度與模型名稱可能改變,授課前仍需再次查核。 + +## 生成式影像 + +- Ho, Jain, and Abbeel, “Denoising Diffusion Probabilistic Models.” +- Rombach et al., “High-Resolution Image Synthesis With Latent Diffusion Models.” +- Hu et al., “LoRA: Low-Rank Adaptation of Large Language Models.” + +## 語音與聲音合成 + +- Jia et al., “Transfer Learning from Speaker Verification to Multispeaker Text-To-Speech Synthesis.” + +## 工具與執行環境 + +- Google Colab FAQ: +- Gemini Apps 圖片生成與編輯說明: +- OpenAI 圖片生成說明: +- FFmpeg 官方文件: +- Pillow 文件: +- OpenCV 文件: +- librosa 文件: + +## 使用方式 + +- 講義只寫本週真正需要的概念,避免把論文摘要整段搬入教材。 +- 公式與模型圖應配合小型實驗,讓學生能從輸入、參數與輸出驗證說法。 +- 任何當學期仍可能變動的服務資訊,皆標記為「授課前需查核」。 + diff --git a/course-115/scripts/build_colab_notebooks.py b/course-115/scripts/build_colab_notebooks.py new file mode 100644 index 0000000..d0ac02f --- /dev/null +++ b/course-115/scripts/build_colab_notebooks.py @@ -0,0 +1,54 @@ +"""把 labs 中的教學腳本包裝為可上傳至 Colab 的單一程式碼 Notebook。""" + +from __future__ import annotations +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +LABS = ROOT / "labs" +OUT = ROOT / "colab" +FILES = { + "W02_Image_Basics.ipynb": "w02_image_basics.py", + "W03_Convolution.ipynb": "w03_convolution.py", + "W04_Diffusion_Forward.ipynb": "w04_diffusion_forward.py", + "W10_Audio_Spectrum.ipynb": "w10_audio_spectrum.py", + "W11_Audio_Edit.ipynb": "w11_audio_edit.py", + "W15_Make_Short.ipynb": "w15_make_short.py", +} + + +def notebook(title: str, script: str) -> dict: + install = "!pip -q install Pillow numpy opencv-python-headless matplotlib librosa soundfile moviepy" + return { + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [f"# {title}\n", "請先上傳講義指定的輸入素材,再依序執行。CPU 可完成;Colab GPU 非必要。\n"], + }, + {"cell_type": "code", "execution_count": None, "metadata": {}, "outputs": [], "source": [install]}, + {"cell_type": "code", "execution_count": None, "metadata": {}, "outputs": [], "source": script.splitlines(True)}, + ], + "metadata": { + "colab": {"provenance": []}, + "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, + "language_info": {"name": "python"}, + }, + "nbformat": 4, + "nbformat_minor": 5, + } + + +def main() -> None: + OUT.mkdir(exist_ok=True) + for notebook_name, script_name in FILES.items(): + source = (LABS / script_name).read_text(encoding="utf-8") + data = notebook(notebook_name.removesuffix(".ipynb").replace("_", " "), source) + (OUT / notebook_name).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + print(notebook_name) + + +if __name__ == "__main__": + main() + diff --git a/course-115/scripts/build_weekly_slides.js b/course-115/scripts/build_weekly_slides.js new file mode 100644 index 0000000..b3cbdd5 --- /dev/null +++ b/course-115/scripts/build_weekly_slides.js @@ -0,0 +1,137 @@ +const fs = require('fs'); +const path = require('path'); +const pptxgen = require('pptxgenjs'); +const html2pptx = require('C:/Users/Willis/.agents/skills/pptx/scripts/html2pptx.js'); + +const ROOT = path.resolve(__dirname, '..'); +const OUT = path.join(ROOT, 'slides'); +const HTML = path.join(OUT, '_html'); +const TMP = path.join(OUT, '_tmp'); + +const weeks = [ + {w:'W01',t:'先拆解,再創作',q:'什麼才算一個多媒體系統?',m:'系統與紀錄',c:'#24d7ff',img:'course-map.png', + goals:['分辨多媒體檔案與多媒體系統','用輸入—處理—輸出—驗證拆解短片','用 Markdown 建立可追溯筆記'], + principles:['媒體管線不是工具清單,而是資料如何流動','輸出要能播放,也要能說明來源與限制','AI 可以協作,但提示、採用與驗證都要留下'], + terms:['input','process','output','validation'],lab:['選一支 15–30 秒短片','記錄圖片、聲音、字幕與時間','畫出最小系統圖','完成 HackMD 個人筆記'], + day:'固定六格觀察表;指出一個可見效果與一個隱藏設定。',night:'拆解工作或生活流程;增加效能限制與權利風險。', + deliver:['HackMD 筆記','媒體拆解表','100–150 字反思'],exit:['檔案與系統差在哪裡?','哪一步最可能出錯?','期末希望觀眾理解什麼?']}, + {w:'W02',t:'數位影像與像素',q:'一張圖片在電腦裡是什麼?',m:'影像模組',c:'#24d7ff',img:'pixel-rgb.png', + goals:['讀取尺寸、模式、格式與像素','解釋 RGB/RGBA 與色彩深度','比較 PNG 與多組 JPEG 品質'], + principles:['解析度是像素數,不等於整體畫質','未壓縮量=寬×高×通道×位元÷8','格式選擇要同時看用途、品質、大小與相容性'], + terms:['pixel','RGB/RGBA','bit depth','lossy/lossless'],lab:['用 Pillow 讀取 input.jpg','輸出 PNG 與 JPEG 95/70/40','記錄檔案大小與可見瑕疵','每次只改一個變因'], + day:'固定輸入與觀察表,回答「最小是否最適合」。',night:'比較照片與文字截圖,分析內容複雜度。',deliver:['程式與四張輸出','格式比較表','150 字結論'],exit:['寬高減半剩多少像素?','透明圖為何不選 JPEG?','格式名稱能保證品質嗎?']}, + {w:'W03',t:'卷積與影像處理',q:'濾鏡為什麼能找出邊緣?',m:'影像模組',c:'#24d7ff',img:'convolution.png', + goals:['手算一個 3×3 卷積','辨認模糊、銳化與邊緣核','解釋 padding 與資料型別'], + principles:['卷積是鄰域加權和,不是魔法按鈕','邊界策略會改變輸出邊緣','負值與大於 255 的結果需要正確處理'], + terms:['kernel','padding','clipping','gradient'],lab:['紙上算 3×3 灰階區塊','用 NumPy 實作卷積','與 OpenCV/Pillow 對照','製作柔和、銳利與線稿三版'], + day:'固定 kernel,只改一個權重或門檻。',night:'建立資料夾批次處理與命名規則。',deliver:['三種濾鏡輸出','參數與時間紀錄','失敗案例'],exit:['平均核為何除以總和?','邊緣等於物體辨識嗎?','整張發黑先查什麼?']}, + {w:'W04',t:'擴散模型原理',q:'AI 如何從雜訊生成圖片?',m:'生成影像',c:'#ef4ed8',img:'diffusion-process.png', + goals:['區分前向加噪與反向生成','解釋時間步條件與雜訊預測','用 CPU 驗證訊號/雜訊比例'], + principles:['前向過程可直接依排程加高斯雜訊','模型常學習 εθ(xₜ,t) 的雜訊估計','生成從隨機雜訊開始,逐步取得較乾淨樣本'], + terms:['DDPM','noise schedule','U-Net','timestep'],lab:['固定 seed=42','比較 ᾱ=.95/.70/.30/.05','輸出連續加噪圖','畫四格技術概念卡'], + day:'固定圖片、seed,只改 ᾱ。',night:'比較線性/餘弦概念排程的加噪速度。',deliver:['CPU 加噪圖','四格概念卡','參數觀察'],exit:['前向加噪需要神經網路嗎?','模型每步預測什麼?','同提示為何有不同結果?']}, + {w:'W05',t:'潛在擴散與提示控制',q:'文字如何控制生成影像?',m:'生成影像',c:'#ef4ed8',img:'latent-diffusion.png', + goals:['說明 VAE 壓縮與解碼','理解文字編碼與交叉注意力','公平比較 prompt、seed、steps、CFG'], + principles:['在 latent 空間去噪可降低空間計算量','交叉注意力把文字條件帶入影像特徵','CFG 過高可能造成失真,steps 也非越多越好'], + terms:['VAE','latent','cross-attention','CFG/seed'],lab:['選中性且可公開的主題','固定比例與可見參數','A/B/C 每次只加一層規格','記錄改善與新失敗'], + day:'固定三段式提示模板,只調整一個段落。',night:'建立三張圖共用的角色與風格規格。',deliver:['三版生成結果','控制變因表','提示與採用紀錄'],exit:['VAE 為何省計算?','seed 不能保證什麼?','CFG 越高一定越好嗎?']}, + {w:'W06',t:'LoRA 與影像微調',q:'影像微調到底改了什麼?',m:'生成影像',c:'#ef4ed8',img:'lora.png', + goals:['區分提示控制與參數更新','用 W′=W+BA 解釋低秩更新','設計合法、可測試的小型資料集'], + principles:['LoRA 凍結原權重,學習小型低秩更新','資料角度、背景與標註決定模型學到什麼','訓練損失下降不等於未見情境表現變好'], + terms:['fine-tuning','rank r','overfitting','hold-out test'],lab:['規劃 12–20 張候選圖','填寫來源、同意與標註資料卡','區分訓練候選與測試提示','判斷欠擬合/適當/過擬合'], + day:'用固定虛構角色找出四類資料問題。',night:'自訂非人物概念,提出測試矩陣與停止條件。',deliver:['LoRA 微調企畫','資料卡','測試與停止條件'],exit:['為何稱為低秩?','小檔案等於低風險嗎?','如何判斷泛化?']}, + {w:'W07',t:'生成影像工作室',q:'如何做出一致而可信的影像系列?',m:'影像作品',c:'#ef4ed8',img:'image-story-workflow.png', + goals:['先建立視覺規格再生成','用版本比較改善一致性','完成尺寸、色調與安全區後製'], + principles:['單張好看不等於能形成影片','固定角色、色盤、構圖語言與字幕空間','AI 輸出是素材;瑕疵與來源仍需人工檢查'], + terms:['visual bible','consistency','safe area','versioning'],lab:['寫故事句與四格分鏡','建立角色/色盤/構圖規格','生成、拒絕、修正並記錄','輸出 3–6 張統一比例圖片'], + day:'固定「狀態—問題—處理—結果」四格。',night:'自訂敘事並建立可重用提示元件。',deliver:['3–6 張影像','視覺規格與分鏡','來源、提示與瑕疵表'],exit:['固定了哪些變因?','哪種一致性最難?','哪張圖最可能造成誤解?']}, + {w:'W08',t:'期中影像展與技術答辯',q:'我能不能解釋作品背後的技術?',m:'期中評量',c:'#ff6b5e',img:'image-showcase.png', + goals:['用三分鐘說明影像作品','以控制實驗支持改善判斷','個別回答影像與生成原理'], + principles:['評量可重現性,不只看華麗程度','展示失敗版比只秀成品更能說明決策','不確定時先區分觀察、推論與待查證'], + terms:['evidence','baseline','failure case','oral defense'],lab:['30 秒問題','60 秒流程','45 秒核心技術','30 秒比較+15 秒限制'], + day:'依固定檢核表說明參數、輸出與錯誤。',night:'補充設計取捨與專題延伸路徑。',deliver:['影像故事組','3–5 頁展示','個人反思'],exit:['哪個證據支持你的選擇?','哪項限制仍存在?','哪些素材進入期末?']}, + {w:'W09',t:'期中考週',q:'本週不授新課',m:'校務週',c:'#7c8799',img:'course-map.png', + goals:['確認班級首頁公告','檢查 W01–W08 檔案可開啟','保留時間準備其他科目'],principles:['不新增技術內容','不新增實作進度','可選整理不列新作業'],terms:['no new lesson'],lab:['確認期中評量安排','整理回饋:保留/修改/待查證','準備下週耳機','不需提前製作後半作品'],day:'依班級公告處理。',night:'依班級公告處理。',deliver:['無新作業'],exit:['下週:數位聲音與頻譜']}, + {w:'W10',t:'數位聲音與頻譜',q:'聲音如何變成可以運算的數字?',m:'聲音模組',c:'#9b6cff',img:'audio-sampling-stft.png', + goals:['解釋取樣率、位元深度與 Nyquist','區分波形、FFT 與 STFT','比較窗長與 hop length'], + principles:['取樣率要高於最高頻率兩倍只是理想基礎','FFT 看整段頻率,STFT 保留局部時間','窗長增加通常改善頻率解析、犧牲時間解析'], + terms:['sample rate','Nyquist','FFT','STFT'],lab:['載入 3–5 秒 WAV','畫波形與 STFT dB 圖','只改 n_fft 或 hop length','比較拍手、母音、環境聲'], + day:'用固定三段音檔辨認時間與頻率特徵。',night:'提出一個必須用 STFT 才容易回答的問題。',deliver:['波形與時頻圖','參數比較','觀察說明'],exit:['44.1 kHz 是可聽上限嗎?','為何要分窗?','窗長如何取捨?']}, + {w:'W11',t:'聲音剪輯與訊號處理',q:'剪輯聲音時,實際改變了什麼?',m:'聲音模組',c:'#9b6cff',img:'audio-editing.png', + goals:['區分裁切、淡化、正規化與壓縮','解釋變速與變調不是同一件事','完成可用於短片的 20–30 秒旁白'], + principles:['淡入淡出是隨時間變化的增益','峰值正規化不等於動態壓縮','極端降噪、time stretch 與 pitch shift 都會產生瑕疵'], + terms:['gain envelope','normalization','compressor','phase vocoder'],lab:['備份原音再裁切','套用淡入淡出與峰值檢查','做一次變速或變調 A/B','以耳機與喇叭複查'], + day:'固定旁白完成三項基本處理。',night:'建立批次腳本與參數停止條件。',deliver:['原音/成品對照','參數與瑕疵紀錄','旁白檔'],exit:['正規化與壓縮差在哪裡?','加快為何會升音?','哪些問題應重錄?']}, + {w:'W12',t:'語音合成與深偽辨識',q:'聲音深偽如何生成,又如何辨識風險?',m:'聲音模組',c:'#9b6cff',img:'voice-synthesis-safety.png', + goals:['區分 TTS、voice cloning、voice conversion','說明 mel、vocoder、speaker embedding','建立多證據查證流程'], + principles:['文字/音素→聲學模型→mel→vocoder→波形','speaker embedding 是說話者特徵表示','偵測器分數只能當線索,不能單獨定論'], + terms:['TTS','mel spectrogram','vocoder','speaker embedding'],lab:['盲聽授權真實/合成音檔','寫判斷、信心與可觀察線索','查看波形與頻譜後修正','提出仍需的外部證據'], + day:'固定音檔比較,排序 TTS 管線。',night:'設計收到可疑語音時的技術+情境查證流程。',deliver:['盲聽紀錄','揭露卡','安全查證流程'],exit:['embedding 是聲音檔嗎?','vocoder 輸入輸出為何?','為何不可單靠偵測分數?']}, + {w:'W13',t:'短影音腳本與數位影片',q:'短影音不是把素材排在一起而已嗎?',m:'影片模組',c:'#ff9f43',img:'short-video-storyboard.png', + goals:['以時間預算寫 15–30 秒腳本','理解 FPS、解析度、timebase 與同步','完成 4–6 格分鏡'], + principles:['FPS 是時間取樣密度,不等於敘事內容量','旁白、字幕與畫面要共享同一時間設計','直式畫面需保留字幕與平台介面安全區'], + terms:['FPS','timebase','9:16','storyboard'],lab:['寫一句核心訊息','朗讀旁白並實際計時','分配 Hook/Context/Process/Result','填入素材、字幕與聲音'], + day:'固定五段時間模板與四格分鏡。',night:'自訂節奏,但需低算力備援與素材風險。',deliver:['15–30 秒旁白','4–6 格分鏡','時間與素材清單'],exit:['影格增加等於內容增加嗎?','字幕安全區在哪裡?','素材不足時先縮小什麼?']}, + {w:'W14',t:'生成內容與素材溯源',q:'如何建立可追溯的多媒體素材庫?',m:'影片模組',c:'#ff9f43',img:'media-provenance.png', + goals:['建立 source/image/audio/subtitle 結構','填寫素材來源、授權、同意與 AI 紀錄','讓素材包可由他人快速接手'], + principles:['素材包是剪輯流程的輸入契約','內嵌 metadata 可能在轉檔或上傳時消失','provenance 應記錄來源、處理與用途鏈'], + terms:['metadata','provenance','asset ID','consent scope'],lab:['整理 8–12 個素材','填 ASSET_LOG.csv','交換素材包做五分鐘驗收','替缺檔或失權素材準備備援'], + day:'使用固定資料夾與表格欄位。',night:'增加版本規則與缺檔自動檢查。',deliver:['完整素材包','ASSET_LOG.csv','AI_USAGE.md'],exit:['metadata 與 provenance 差在哪?','為何不覆寫原始檔?','哪個素材失權就要換?']}, + {w:'W15',t:'FFmpeg 與短影音合成',q:'影片如何被合成與壓縮?',m:'影片模組',c:'#ff9f43',img:'video-codec-gop.png', + goals:['分辨 container 與 codec','理解 I/P/B frame、GOP 與 bitrate','輸出 720×1280 MP4 粗剪版'], + principles:['MP4 是容器,H.264 是視訊編碼方式','編碼利用影格內與影格間冗餘','關鍵影格密度、bitrate、內容動態與品質互相牽動'], + terms:['container','codec','keyframe/GOP','CRF/bitrate'],lab:['用 ffprobe 讀取 streams','維持比例縮放並補邊','合成圖片、旁白與字幕','換兩個 CRF 比較大小與瑕疵'], + day:'固定四張圖與旁白完成組裝。',night:'依自選分鏡建立可重跑腳本與 CPU 降級路徑。',deliver:['720×1280 rough cut','ffprobe 紀錄','兩組壓縮比較'],exit:['MP4 與 H.264 能互換嗎?','關鍵影格越多越好嗎?','為何不可直接拉伸?']}, + {w:'W16',t:'進度小報告與系統測試',q:'作品是否已經成為一個系統?',m:'專題整合',c:'#ff6b5e',img:'final-project-pipeline.png', + goals:['播放從頭到尾可運作的 rough cut','用架構圖說明資料流','完成正常、邊界與錯誤測試'], + principles:['先修不能播放、看不懂、聽不清楚','再修來源、同意、揭露與可重現性','最後才增加轉場、動畫與裝飾'], + terms:['rough cut','architecture','edge case','fallback'],lab:['30 秒問題+60 秒播放','60 秒架構+60 秒技術比較','45 秒權利與 AI 揭露','45 秒風險與期限'], + day:'固定表逐組檢查播放、聲畫、字幕與來源。',night:'說明刪除或延後一項功能如何降低風險。',deliver:['Rough cut','一頁架構圖','三類測試與任務表'],exit:['哪個錯誤最先修?','雲端失效時怎麼辦?','期末前只保留哪三件事?']}, + {w:'W17',t:'期末成果發表 A',q:'如何讓觀眾相信我的技術說明?',m:'期末發表',c:'#ff6b5e',img:'final-project-pipeline.png', + goals:['完整播放與個別答辯','連結作品流程與技術原理','給出可操作的同儕回饋'],principles:['不以特效數與生成量評分','每項主張要有作品或紀錄支持','個人仍需說明自己的決策、失敗與限制'],terms:['demo','evidence','oral defense','peer review'],lab:['播放作品','說明問題與管線','展示技術比較與失敗','回答個別原理問題'],day:'依固定答辯範圍準備。',night:'補充整合取捨與實務限制。',deliver:['完整作品','答辯','兩組同儕回饋'],exit:['哪個證據最有說服力?','哪個問題仍待改善?']}, + {w:'W18',t:'成果發表 B 與封存',q:'如何從成果回推可改進的系統?',m:'期末發表',c:'#ff6b5e',img:'final-project-pipeline.png', + goals:['完成第二梯次發表','封存素材、程式與紀錄','用具體案例完成個人反思'],principles:['雲端網址與免費額度可能失效','應保留原始素材、開放紀錄與可重現步驟','反思要區分觀察、判斷、限制與下一步'],terms:['archive','README','AI_USAGE','reflection'],lab:['完整播放與答辯','確認 final.mp4','檢查 README/ASSET_LOG/AI_USAGE','完成五題個人反思'],day:'依封存清單逐項驗收。',night:'補充未來部署或再利用條件。',deliver:['完整專題資料夾','個人反思','可開啟的最終連結'],exit:['哪個概念改變了決策?','哪次 AI 建議被拒絕?','公開前還要查什麼?']}, +]; + +function esc(s){return String(s).replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));} +function dataUri(file){const p=path.join(ROOT,'assets','diagrams',file);return `data:image/png;base64,${fs.readFileSync(p).toString('base64')}`;} +function baseCss(color){return ` +*{box-sizing:border-box} html,body{margin:0;width:720pt;height:405pt;overflow:hidden;background:#11151d;color:#f5f7fb;font-family:Arial,sans-serif} +body{position:relative} .slide{width:720pt;height:405pt;padding:30pt 34pt;position:relative;overflow:hidden} +.top{display:flex;align-items:center;justify-content:space-between;margin-bottom:18pt}.week{font:700 12pt 'Courier New',monospace;color:${color};letter-spacing:1.2pt}.module{font-size:10pt;color:#aab3c3} +h1{font-size:30pt;line-height:1.08;margin:0 0 10pt 0;letter-spacing:-.5pt} h2{font-size:21pt;line-height:1.12;margin:0 0 14pt 0} h3{font-size:13pt;margin:0 0 8pt 0;color:${color}} +p{font-size:13pt;line-height:1.35;margin:0}.muted{color:#aab3c3}.accent{color:${color}} .small{font-size:9.5pt;line-height:1.3} +.rule{height:3pt;width:70pt;background:${color};margin:13pt 0}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18pt}.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:12pt} +.card{background:#1a202b;border:1pt solid #313a49;border-radius:10pt;padding:14pt}.card.ac{border-color:${color}} .num{width:28pt;height:28pt;border-radius:50%;background:${color};color:#10141c;display:flex;align-items:center;justify-content:center;margin-bottom:8pt}.num p{font-weight:800;font-size:13pt} +ul{margin:4pt 0 0 17pt;padding:0}li{font-size:12pt;line-height:1.32;margin-bottom:6pt}.chips{display:flex;gap:7pt;flex-wrap:wrap}.chip{border:1pt solid ${color};border-radius:20pt;padding:5pt 9pt;background:#151b24}.chip p{font:700 9pt 'Courier New',monospace;color:${color}} +.hero{display:grid;grid-template-columns:42% 58%;gap:25pt;align-items:center;height:320pt}.heroImg{width:100%;height:285pt;object-fit:cover;border-radius:13pt;border:1pt solid #30394a} +.wideImg{width:100%;height:185pt;object-fit:cover;border-radius:12pt;border:1pt solid #30394a}.footer{position:absolute;bottom:16pt;left:34pt;right:34pt;display:flex;justify-content:space-between}.footer p{font:8pt 'Courier New',monospace;color:#697386} +.split{display:grid;grid-template-columns:1fr 1fr;gap:16pt;margin-top:18pt}.split .card{min-height:130pt}.tag{font:700 10pt 'Courier New',monospace;color:${color};margin-bottom:7pt}.question{font-size:19pt;line-height:1.22;color:#dfe5ef} +`} +function page(w,body){return `
${body}
`;} +function top(w){return `

${esc(w.w)}

${esc(w.m)}

`;} +function bullets(items){return `
    ${items.map(x=>`
  • ${esc(x)}
  • `).join('')}
`;} + +function slidesFor(w){ + const img=dataUri(w.img); const slides=[]; + slides.push(page(w,`
${top(w)}

${esc(w.t)}

${esc(w.q)}

從原理、實驗到可說明的作品

`)); + slides.push(page(w,`${top(w)}

本週學習地圖

${w.goals.map((x,i)=>`

${i+1}

${esc(x)}

`).join('')}
`)); + slides.push(page(w,`${top(w)}

核心原理

${w.principles.map((x,i)=>`

0${i+1}

${esc(x)}

`).join('')}

關鍵詞

${w.terms.map(x=>`

${esc(x)}

`).join('')}

說明規則

指出輸入、改變的參數、觀察到的輸出,以及仍不能確定的部分。

`)); + slides.push(page(w,`${top(w)}

課堂實驗/任務

${w.lab.map((x,i)=>`

${i+1}

${esc(x)}

`).join('')}
`)); + if(w.w!=='W09') slides.push(page(w,`${top(w)}

同一核心,兩種支架

日間引導

${esc(w.day)}

進修挑戰

${esc(w.night)}

共同底線

CPU 路徑可完成;AI 生成需記錄;素材需有來源與同意;每位學生都能說明關鍵原理。

`)); + slides.push(page(w,`${top(w)}

本週完成條件

繳交

${bullets(w.deliver)}

Exit Ticket

${bullets(w.exit)}

不要只交成品:留下變因、觀察、判斷與限制,才能把作品變成可學習的系統。

`)); + return slides; +} + +async function buildDeck(w){ + const deckDir=path.join(HTML,w.w); fs.mkdirSync(deckDir,{recursive:true}); + const htmlSlides=slidesFor(w); const files=[]; + htmlSlides.forEach((html,i)=>{const f=path.join(deckDir,`slide-${String(i+1).padStart(2,'0')}.html`);fs.writeFileSync(f,html);files.push(f)}); + const pptx=new pptxgen(); pptx.layout='LAYOUT_16X9'; pptx.author='Willis Max'; pptx.subject='多媒體系統 115'; pptx.title=`${w.w}|${w.t}`; pptx.company='課程教材'; pptx.lang='zh-TW'; + pptx.defineSlideMaster({title:'BASE',background:{color:'11151D'},objects:[],slideNumber:{x:12.45,y:7.1,w:.35,h:.18,color:'697386',fontFace:'Arial',fontSize:7}}); + for(const file of files){await html2pptx(file,pptx,{tmpDir:TMP});} + const out=path.join(OUT,`${w.w}_${w.t.replace(/[\\/:*?"<>|]/g,'_')}.pptx`); await pptx.writeFile({fileName:out}); console.log(out); +} + +(async()=>{fs.mkdirSync(OUT,{recursive:true});fs.mkdirSync(TMP,{recursive:true});for(const w of weeks)await buildDeck(w);})().catch(e=>{console.error(e);process.exit(1)}); + diff --git a/course-115/scripts/export_pptx_previews.ps1 b/course-115/scripts/export_pptx_previews.ps1 new file mode 100644 index 0000000..dccb064 --- /dev/null +++ b/course-115/scripts/export_pptx_previews.ps1 @@ -0,0 +1,27 @@ +$ErrorActionPreference = 'Stop' +$courseRoot = Split-Path -Parent $PSScriptRoot +$slidesRoot = Join-Path $courseRoot 'slides' +$exportRoot = Join-Path $slidesRoot 'previews\exported' +New-Item -ItemType Directory -Force -Path $exportRoot | Out-Null + +$powerPoint = New-Object -ComObject PowerPoint.Application +try { + foreach ($deck in Get-ChildItem -LiteralPath $slidesRoot -Filter '*.pptx' | Sort-Object Name) { + $target = Join-Path $exportRoot $deck.BaseName + New-Item -ItemType Directory -Force -Path $target | Out-Null + $presentation = $powerPoint.Presentations.Open($deck.FullName, $true, $true, $false) + try { + # 18 = ppSaveAsPNG + $presentation.SaveAs($target, 18) + Write-Output $deck.Name + } + finally { + $presentation.Close() + } + } +} +finally { + $powerPoint.Quit() + [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($powerPoint) | Out-Null +} + diff --git a/course-115/scripts/make_slide_montages.py b/course-115/scripts/make_slide_montages.py new file mode 100644 index 0000000..8480950 --- /dev/null +++ b/course-115/scripts/make_slide_montages.py @@ -0,0 +1,45 @@ +"""將 PowerPoint 匯出的 Slide*.PNG 組成每週一張縮圖總覽。""" + +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + + +ROOT = Path(__file__).resolve().parents[1] / "slides" / "previews" +SOURCE = ROOT / "exported" +OUTPUT = ROOT / "montages" + + +def natural_key(path: Path) -> int: + digits = "".join(ch for ch in path.stem if ch.isdigit()) + return int(digits or 0) + + +def main() -> None: + OUTPUT.mkdir(parents=True, exist_ok=True) + font = ImageFont.load_default() + for deck in sorted(path for path in SOURCE.iterdir() if path.is_dir()): + slides = sorted(list(deck.glob("Slide*.PNG")) + list(deck.glob("投影片*.PNG")), key=natural_key) + if not slides: + slides = sorted(deck.glob("*.PNG"), key=natural_key) + if not slides: + continue + columns, thumb_width = 3, 480 + opened = [Image.open(slide).convert("RGB") for slide in slides] + thumb_height = round(thumb_width * opened[0].height / opened[0].width) + rows = (len(opened) + columns - 1) // columns + sheet = Image.new("RGB", (columns * thumb_width, rows * (thumb_height + 28)), "#0b0e14") + draw = ImageDraw.Draw(sheet) + for index, image in enumerate(opened): + image.thumbnail((thumb_width, thumb_height)) + x = (index % columns) * thumb_width + y = (index // columns) * (thumb_height + 28) + sheet.paste(image, (x, y)) + draw.text((x + 8, y + thumb_height + 6), f"Slide {index + 1}", fill="white", font=font) + output = OUTPUT / f"{deck.name}.jpg" + sheet.save(output, quality=88) + print(output) + + +if __name__ == "__main__": + main() + diff --git a/course-115/scripts/run_weekly_slides_v4.js b/course-115/scripts/run_weekly_slides_v4.js new file mode 100644 index 0000000..c09a816 --- /dev/null +++ b/course-115/scripts/run_weekly_slides_v4.js @@ -0,0 +1,15 @@ +// 最終相容啟動器:HTML 只負責文字與版面,PNG 由 PptxGenJS 直接加入。 +const fs = require('fs'); +const path = require('path'); +let source = fs.readFileSync(path.join(__dirname, 'build_weekly_slides.js'), 'utf8'); +source = source.replace("LAYOUT_16X9", "LAYOUT_16x9"); +source = source.replace("const HTML = path.join(OUT, '_html');", "const HTML = 'C:/tmp/media-course-slides/html';"); +source = source.replace("const TMP = path.join(OUT, '_tmp');", "const TMP = 'C:/tmp/media-course-slides/tmp';"); +source = source.replace(//g, '
'); +source = source.replace(//g, '
'); +source = source.replace( + "for(const file of files){await html2pptx(file,pptx,{tmpDir:TMP});}", + "for(let i=0;i 同意必須針對明確用途。一般拍攝同意不自動包含聲音克隆、臉部生成或公開發布。 + diff --git a/course-115/templates/FINAL_RUBRIC.md b/course-115/templates/FINAL_RUBRIC.md new file mode 100644 index 0000000..9360a5f --- /dev/null +++ b/course-115/templates/FINAL_RUBRIC.md @@ -0,0 +1,13 @@ +# 期末短影音評量規準 + +| 面向 | 比例 | 可觀察證據 | +|---|---:|---| +| 核心訊息與敘事 | 20% | 15–30 秒內訊息清楚,畫面、旁白與字幕一致 | +| 多媒體技術 | 25% | 正確使用並說明影像、聲音、影片的關鍵處理 | +| 系統與可重現性 | 20% | 架構、檔案、程式/步驟、參數與錯誤處理完整 | +| 實驗與驗證 | 15% | 有控制變因、失敗案例、測試與改善依據 | +| 來源、同意與 AI 揭露 | 10% | 素材可追溯,使用範圍合理,生成內容清楚標示 | +| 個人答辯與反思 | 10% | 能說明分工、決策、限制與下一步 | + +視覺華麗、生成張數或付費工具不另外加分。 + diff --git "a/course-115/weeks/W01_\350\252\262\347\250\213\345\225\237\345\213\225\350\210\207\345\244\232\345\252\222\351\253\224\347\263\273\347\265\261.md" "b/course-115/weeks/W01_\350\252\262\347\250\213\345\225\237\345\213\225\350\210\207\345\244\232\345\252\222\351\253\224\347\263\273\347\265\261.md" new file mode 100644 index 0000000..7d7b144 --- /dev/null +++ "b/course-115/weeks/W01_\350\252\262\347\250\213\345\225\237\345\213\225\350\210\207\345\244\232\345\252\222\351\253\224\347\263\273\347\265\261.md" @@ -0,0 +1,98 @@ +# W01|先拆解,再創作:多媒體系統與 HackMD + +> 本週任務:建立個人課程筆記,拆解一支 15–30 秒短影音的輸入、處理、輸出與限制。 + +![多媒體系統課程地圖](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/course-map.png) + +## 這門課要做什麼? + +多媒體系統不是「會剪片」或「會叫 AI 生圖」而已。一個能運作的系統,至少要回答四個問題: + +1. **輸入是什麼?** 圖片、錄音、文字、影片,格式與來源為何? +2. **做了哪些處理?** 裁切、濾鏡、辨識、生成、壓縮或合成? +3. **輸出給誰使用?** 手機觀眾、課堂展示、社群平台或其他程式? +4. **如何驗證?** 畫質、聲音、字幕、授權、執行時間與失敗情況是否可接受? + +本學期會沿著三條路徑前進:圖片從像素走到擴散模型與 LoRA;聲音從取樣走到剪輯、語音合成與深偽辨識;最後把影像、旁白、音效與字幕合成短影音。 + +## Markdown 是教材的原始格式 + +HackMD 讓我們共同閱讀與討論,Markdown 則讓內容能保存到 GitHub、比較版本,也能轉成簡報或網站。 + +```markdown +# 我的多媒體系統筆記 + +## 本週觀察 + +- 輸入:三張圖片、一段旁白 +- 處理:裁切、字幕、轉場、背景音樂 +- 輸出:直式 MP4 + +| 項目 | 我觀察到的設定 | +|---|---| +| 畫面比例 | 9:16 | +| 時間長度 | 22 秒 | + +> 我最想知道:為什麼壓縮後還能維持清楚? +``` + +### 必會語法 + +- 標題:`#`、`##`、`###` +- 清單:`-` 或 `1.` +- 連結:`[文字](網址)` +- 圖片:`![替代文字](圖片網址)` +- 程式碼:三個反引號包住程式 +- 引用:行首使用 `>` +- 表格:用 `|` 分隔欄位 + +替代文字不是裝飾。圖片無法顯示或使用螢幕閱讀器時,它應該能交代圖片的重要資訊。 + +## 課堂活動:拆解一支短影音 + +選擇一支適合課堂公開討論的短影音,以表格記錄: + +| 層次 | 觀察問題 | +|---|---| +| 素材 | 有幾張圖、幾段聲音、是否有真人? | +| 時間 | 每個鏡頭約幾秒?字幕何時出現? | +| 處理 | 有濾鏡、去背、生成、配音或轉場嗎? | +| 輸出 | 解析度、比例、檔案格式可能是什麼? | +| 風險 | 素材是否有來源?觀眾會不會誤以為是真實事件? | + +把觀察畫成最小系統圖: + +```text +文字/照片/錄音 → 整理與處理 → 時間軸合成 → 壓縮輸出 → 播放與檢查 +``` + +## AI 使用紀錄 + +本課程允許使用 ChatGPT、Gemini 或其他生成式 AI,但每次重要使用都要留下: + +- 你真正送出的提示與素材種類。 +- AI 回覆中採用與拒絕的部分。 +- 你如何驗證程式、資料或媒體輸出。 +- 工具、日期與重要參數;不要抄錄密碼或 API 金鑰。 + +## 分流任務 + +### 日間引導 + +使用教師提供的短片,完成固定六格觀察表;至少指出一個「看得到」的效果和一個「看不到但系統必須處理」的設定。 + +### 進修挑戰 + +選擇工作或生活中的多媒體流程,額外標出一個效能限制與一個權利/倫理風險。 + +## 繳交 + +一份可閱讀的 HackMD 個人筆記,包含標題、表格、連結、圖片替代文字、系統圖與 100–150 字反思。不要在公開筆記放個資、帳號資訊或未獲授權素材。 + +## Exit Ticket + +1. 一個多媒體「檔案」與多媒體「系統」有何不同? +2. 你的系統圖中,哪一步最可能出錯?怎麼知道它出錯? +3. 你希望期末作品讓觀眾理解什麼? + + diff --git "a/course-115/weeks/W02_\346\225\270\344\275\215\345\275\261\345\203\217\350\210\207\345\203\217\347\264\240.md" "b/course-115/weeks/W02_\346\225\270\344\275\215\345\275\261\345\203\217\350\210\207\345\203\217\347\264\240.md" new file mode 100644 index 0000000..02859e2 --- /dev/null +++ "b/course-115/weeks/W02_\346\225\270\344\275\215\345\275\261\345\203\217\350\210\207\345\203\217\347\264\240.md" @@ -0,0 +1,82 @@ +# W02|一張圖片在電腦裡是什麼? + +> 本週任務:用程式讀取同一張圖的尺寸、模式與像素,完成格式與壓縮比較。 + +![像素與 RGB 通道示意](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/pixel-rgb.png) + +## 從連續世界到像素格 + +數位影像把連續的光線取樣成規則格點。每個格點是像素;解析度 `1920×1080` 表示水平方向 1920 個、垂直方向 1080 個像素,不等於畫面一定清楚。鏡頭品質、對焦、壓縮與顯示尺寸也會影響結果。 + +常見模式: + +- `L`:灰階,通常一個像素一個亮度值。 +- `RGB`:紅、綠、藍三個通道。 +- `RGBA`:多一個 alpha 透明度通道。 + +若每個 RGB 通道使用 8 位元,一個像素可表示約 1677 萬種組合。未壓縮資料量可先用下式估算: + +```text +寬 × 高 × 通道數 × 每通道位元數 ÷ 8 +``` + +實際 PNG、JPEG、WebP 大小會受壓縮方式與內容複雜度影響。 + +## 無失真與有失真 + +- **PNG** 常用無失真壓縮,適合介面、線條、透明背景與需反覆編輯的素材。 +- **JPEG** 以有失真方式換取較小檔案,適合照片;反覆另存可能累積壓縮痕跡。 +- **WebP** 可支援有失真、無失真與透明度,但工作流程仍要確認軟體相容性。 + +格式名稱不是品質保證。真正要比較的是用途、設定、視覺差異、檔案大小與相容性。 + +## CPU 實驗:讀取影像資訊 + +請執行 `labs/w02_image_basics.py`。核心程式如下: + +```python +from pathlib import Path +from PIL import Image + +path = Path("input.jpg") +img = Image.open(path) +print("尺寸:", img.size) +print("模式:", img.mode) +print("格式:", img.format) +print("左上角像素:", img.convert("RGB").getpixel((0, 0))) +``` + +再將同一張圖輸出為 PNG,以及品質 95、70、40 的 JPEG。每次只改一個變因,記錄檔案大小與你真正看見的差異。 + +| 格式/品質 | 檔案大小 | 邊緣 | 漸層 | 適合用途 | +|---|---:|---|---|---| +| PNG | | | | | +| JPEG 95 | | | | | +| JPEG 70 | | | | | +| JPEG 40 | | | | | + +## 觀念檢核 + +1. 解析度減半時,像素總數不只減半;寬與高都減半會剩下四分之一。 +2. 通道不是三張互不相關的圖片,而是共同描述每個位置的顏色。 +3. 顯示器看到的顏色還會受到色彩空間與裝置影響;本週先以 RGB 數值觀察。 + +## 分流任務 + +### 日間引導 + +使用同一張教師範例圖,完成四個輸出與固定觀察表。口頭回答「檔案大小最小」是否等於「最適合」。 + +### 進修挑戰 + +自選兩種內容差異明顯的圖片,例如人像與文字截圖,比較內容複雜度如何影響壓縮結果。 + +## 繳交與 Exit Ticket + +繳交程式、四張輸出、觀察表與 150 字結論。離開前回答:如果要把一張透明圖示放進短影音,為什麼可能不選 JPEG? + +## 延伸來源 + +- [Pillow 文件](https://pillow.readthedocs.io/) + + diff --git "a/course-115/weeks/W03_\345\215\267\347\251\215\350\210\207\345\275\261\345\203\217\350\231\225\347\220\206.md" "b/course-115/weeks/W03_\345\215\267\347\251\215\350\210\207\345\275\261\345\203\217\350\231\225\347\220\206.md" new file mode 100644 index 0000000..0b9ae50 --- /dev/null +++ "b/course-115/weeks/W03_\345\215\267\347\251\215\350\210\207\345\275\261\345\203\217\350\231\225\347\220\206.md" @@ -0,0 +1,87 @@ +# W03|濾鏡為什麼能找出邊緣? + +> 本週任務:在 CPU 上實作卷積,觀察模糊、銳化與邊緣偵測如何改變像素。 + +![卷積核掃過影像的示意](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/convolution.png) + +## 卷積不是神奇濾鏡 + +卷積核是一個小矩陣。它在影像上逐格移動,把鄰近像素乘上對應權重後相加,產生新的像素值。對位置 `(i, j)`,可以概略寫成: + +```text +output[i, j] = Σ image[i+m, j+n] × kernel[m, n] +``` + +不同權重對應不同效果: + +```python +blur = [[1, 1, 1], + [1, 1, 1], + [1, 1, 1]] # 最後除以 9 + +sharpen = [[ 0, -1, 0], + [-1, 5, -1], + [ 0, -1, 0]] + +edge = [[-1, -1, -1], + [-1, 8, -1], + [-1, -1, -1]] +``` + +模糊核做鄰域平均;銳化核強調中心與周圍的差;邊緣核對亮度快速變化的位置產生較大反應。 + +## 三個容易忽略的問題 + +### 邊界怎麼辦? + +卷積核移到影像邊緣時,部分位置沒有像素。程式可選擇補零、複製邊界、鏡射或縮小輸出。不同策略會造成不同邊緣結果。 + +### 數值會超出範圍嗎? + +8 位元通道通常在 0–255。加權結果可能小於 0 或大於 255,需要截斷、正規化或改用較高精度計算後再轉回。 + +### RGB 每個通道都要算嗎? + +可以分別卷積,也可先轉灰階。兩種做法回答的問題不同;邊緣偵測通常關心亮度變化,但彩色資訊也可能重要。 + +## 實驗:自己算一次,再交給函式庫 + +1. 先在紙上用一個 `3×3` 灰階區塊計算中心輸出。 +2. 執行 `labs/w03_convolution.py`,以 NumPy 完成相同計算。 +3. 再使用 Pillow 或 OpenCV 套用整張影像。 +4. 對照自己與函式庫結果;若不同,檢查邊界與資料型別。 + +```python +import cv2 + +img = cv2.imread("input.jpg") +gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) +edges = cv2.Canny(gray, 80, 160) +cv2.imwrite("edges.png", edges) +``` + +`Canny` 不只是一個卷積核,它還包含平滑、梯度、非極大值抑制與雙門檻等步驟。本週重點是看懂它建立在局部變化之上,而不是背函式名稱。 + +## 創作任務 + +把同一張圖做成「柔和、銳利、線稿」三個版本。每個版本要記錄方法、參數、處理時間與失敗情況。不要用三張不同輸入圖混淆比較。 + +### 日間引導 + +使用固定核與固定輸入,修改一次權重或門檻,說明輸出改變。 + +### 進修挑戰 + +建立可一次處理資料夾內多張圖片的流程,並保留原始檔與輸出命名規則。 + +## Exit Ticket + +1. 為什麼平均核要除以權重總和? +2. 邊緣偵測是在辨識物體,還是在找局部變化? +3. 若結果整張發黑,最先檢查哪兩件事? + +## 延伸來源 + +- [OpenCV 文件](https://docs.opencv.org/) + + diff --git "a/course-115/weeks/W04_\346\223\264\346\225\243\346\250\241\345\236\213\345\216\237\347\220\206.md" "b/course-115/weeks/W04_\346\223\264\346\225\243\346\250\241\345\236\213\345\216\237\347\220\206.md" new file mode 100644 index 0000000..769b8e6 --- /dev/null +++ "b/course-115/weeks/W04_\346\223\264\346\225\243\346\250\241\345\236\213\345\216\237\347\220\206.md" @@ -0,0 +1,71 @@ +# W04|AI 如何從雜訊生成圖片? + +> 本週任務:用少量程式模擬前向加噪,並用自己的話解釋反向去噪模型學了什麼。 + +![擴散模型的前向加噪與反向去噪](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/diffusion-process.png) + +## 先把圖破壞,再學著復原 + +去噪擴散機率模型把生成拆成多個小步驟。前向過程逐步加入高斯雜訊,直到影像接近隨機雜訊;訓練時,神經網路看見帶雜訊的樣本與時間步,學習預測加入的雜訊。生成時從隨機雜訊開始,反覆使用模型得到較乾淨的樣本。 + +常見的封閉形式可寫成: + +```text +x_t = √(ᾱ_t) x_0 + √(1 − ᾱ_t) ε +``` + +- `x_0`:原始影像。 +- `x_t`:第 `t` 步的帶雜訊影像。 +- `ε`:取樣自標準常態分布的雜訊。 +- `ᾱ_t`:控制還保留多少原始訊號。 + +當 `t` 增加,原始訊號比例通常下降。模型常以 `ε_θ(x_t, t)` 預測雜訊,透過預測誤差學習。實際模型與取樣器有許多變形,本週先掌握「時間步條件下的雜訊估計」。 + +## U-Net 在做什麼? + +U-Net 類架構會先縮小空間尺寸、擷取高階特徵,再放大回原尺寸;跨層連接把較細的空間資訊帶回解碼端。時間步會以嵌入加入網路,使同一模型知道目前面對的是輕度或重度雜訊。 + +這不代表模型把訓練圖片存在某個資料夾再拼貼。它學到的是參數化的統計關係,但仍可能重現偏見、常見構圖,甚至在資料與訓練條件下產生近似記憶的結果,因此資料來源與輸出檢查不能省略。 + +## CPU 實驗:只做前向加噪 + +執行 `labs/w04_diffusion_forward.py`,固定同一個 `seed`,比較 `ᾱ_t` 為 0.95、0.70、0.30、0.05 時的結果。 + +```python +import numpy as np + +rng = np.random.default_rng(42) +noise = rng.normal(0, 1, image.shape) +xt = np.sqrt(alpha_bar) * image + np.sqrt(1-alpha_bar) * noise +``` + +本實驗不是完整生成模型,但能驗證公式中兩個權重如何改變訊號與雜訊的比例。免費 GPU 不穩定時,仍可完整完成本週必做學習。 + +## 技術概念卡 + +請畫一張四格概念卡,依序回答: + +1. 前向過程的輸入與輸出。 +2. 訓練模型看見哪些資料。 +3. 模型預測什麼。 +4. 生成時為什麼能從雜訊開始。 + +### 日間引導 + +使用固定圖片與固定 seed,只改 `ᾱ_t`,完成訊號比例觀察表。 + +### 進修挑戰 + +改用線性或餘弦概念排程產生 8–12 張連續影格,討論排程如何改變加噪速度;不要求訓練模型。 + +## Exit Ticket + +1. 擴散模型的前向過程需要神經網路嗎? +2. 生成時模型通常預測的是最終圖片,還是每一步要移除的成分? +3. 相同提示為什麼仍可能產生不同結果? + +## 主要來源 + +- [Denoising Diffusion Probabilistic Models](https://arxiv.org/abs/2006.11239) + + diff --git "a/course-115/weeks/W05_\346\275\233\345\234\250\346\223\264\346\225\243\350\210\207\346\217\220\347\244\272\346\216\247\345\210\266.md" "b/course-115/weeks/W05_\346\275\233\345\234\250\346\223\264\346\225\243\350\210\207\346\217\220\347\244\272\346\216\247\345\210\266.md" new file mode 100644 index 0000000..4859de9 --- /dev/null +++ "b/course-115/weeks/W05_\346\275\233\345\234\250\346\223\264\346\225\243\350\210\207\346\217\220\347\244\272\346\216\247\345\210\266.md" @@ -0,0 +1,77 @@ +# W05|文字如何控制生成影像? + +> 本週任務:以固定 seed 做提示與參數對照,說明 VAE、文字條件與引導強度各自扮演的角色。 + +![潛在擴散模型管線](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/latent-diffusion.png) + +## 為什麼不直接在所有像素上去噪? + +高解析圖片包含大量像素。潛在擴散模型先使用自編碼器把圖片壓縮成較小的潛在表示,在潛在空間進行擴散,再把結果解碼回圖片。這能降低運算量,但潛在空間是模型學到的表示,不是一般 ZIP 壓縮,也不是人可以直接閱讀的縮圖。 + +一個簡化管線是: + +```text +文字 → tokenizer/文字編碼器 ─┐ + ├→ 潛在空間反覆去噪 → VAE 解碼 → 圖片 +隨機 seed → 初始潛在雜訊 ─────┘ +``` + +交叉注意力讓影像特徵在去噪過程中參考文字條件。它能建立詞彙與視覺模式的關聯,但不保證正確理解否定、數量、空間關係或專有名詞。 + +## 四個值得控制的變因 + +- **提示文字**:主體、動作、環境、構圖、光線、媒材與限制。 +- **seed**:初始化隨機狀態;固定它有助於做對照,但不同服務或模型不一定跨平台重現。 +- **steps**:取樣步數;增加不一定一直改善,且會增加時間。 +- **guidance/CFG**:提高文字條件影響。過高可能造成過度飽和、失真或構圖僵硬。 + +概念上,classifier-free guidance 會組合「有條件」與「無條件」預測,放大兩者差異。不同工具可能使用不同名稱或不公開參數,紀錄時要寫清楚實際介面。 + +## 公平的提示實驗 + +使用 ChatGPT 或 Gemini 免費版做少量生成。服務介面與額度可能變動,授課前以現場可用功能為準。 + +1. 選一個中性、可公開的主題。 +2. 先固定 seed(若工具提供)與畫面比例。 +3. 版本 A 只寫主體;版本 B 加入環境;版本 C 再加構圖與光線。 +4. 不要一次改提示、模型、比例與風格。 +5. 記錄看得見的改善與新的失敗。 + +| 版本 | 唯一改變 | 一致性 | 可讀性 | 瑕疵 | 是否採用 | +|---|---|---:|---:|---|---| +| A | | | | | | +| B | | | | | | +| C | | | | | | + +## 提示不是魔法咒語 + +好的提示應像設計規格,而不是堆滿形容詞。先寫「要解決的畫面問題」,再寫可觀察條件。例如: + +```text +直式 9:16 教育短片插圖。一名虛構學生在桌面觀察 RGB 像素方格, +俯視構圖,清楚留出上方字幕空間,扁平幾何插畫,不含品牌與文字。 +``` + +## 分流任務 + +### 日間引導 + +使用教師指定主題與三段式提示模板,只調整一個段落,完成對照表。 + +### 進修挑戰 + +自行建立風格規格表,測試同一規格能否維持三張圖的一致角色、色盤與構圖語言。 + +## Exit Ticket + +1. VAE 為何能降低擴散過程的計算成本? +2. 固定 seed 能控制什麼,不能保證什麼? +3. CFG 越高是否一定越符合提示? + +## 主要來源 + +- [High-Resolution Image Synthesis With Latent Diffusion Models](https://openaccess.thecvf.com/content/CVPR2022/html/Rombach_High-Resolution_Image_Synthesis_With_Latent_Diffusion_Models_CVPR_2022_paper.html) +- [OpenAI 圖片生成說明](https://help.openai.com/en/articles/11084440) +- [Gemini Apps 圖片生成與編輯說明](https://support.google.com/gemini/answer/14286560) + + diff --git "a/course-115/weeks/W06_LoRA\350\210\207\345\275\261\345\203\217\345\276\256\350\252\277.md" "b/course-115/weeks/W06_LoRA\350\210\207\345\275\261\345\203\217\345\276\256\350\252\277.md" new file mode 100644 index 0000000..34bee6e --- /dev/null +++ "b/course-115/weeks/W06_LoRA\350\210\207\345\275\261\345\203\217\345\276\256\350\252\277.md" @@ -0,0 +1,71 @@ +# W06|影像微調到底改了什麼? + +> 本週任務:設計一份合法、可追溯的小型資料集與 LoRA 微調企畫;實際訓練為選配。 + +![LoRA 低秩更新概念](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/lora.png) + +## 微調不是把參考圖放進提示 + +提示控制不改變模型參數;微調則使用新資料更新部分或全部參數,使模型較容易產生特定概念、角色或視覺語言。完整微調成本高,也更容易破壞原有能力。LoRA 的做法是保留原權重 `W`,另外學習低秩更新: + +```text +W′ = W + ΔW,且 ΔW = B A +``` + +若 `W` 很大,而 `A`、`B` 的秩 `r` 很小,需要訓練與保存的參數就能大幅減少。這是參數效率的核心直覺。實際影像模型會選擇特定層套用 LoRA;檔案小不代表訓練資料或權利風險也變小。 + +## 資料比按下 Train 更重要 + +一份可用的資料卡至少要有: + +- 目標:要學的是角色、物件、服裝、構圖還是風格? +- 來源與授權:每張圖從哪裡來,是否能用於訓練? +- 同意:可識別人物是否明確同意這個用途? +- 多樣性:角度、背景、光線是否足夠,還是每張都幾乎相同? +- 品質:模糊、水印、文字、錯誤裁切是否會被模型一併學走? +- 標註:caption 是否描述真正要學的特徵? +- 保留測試:哪些圖片不參與訓練,用來觀察泛化? + +## 過擬合的訊號 + +- 不同提示仍產生近似背景或姿勢。 +- 訓練圖的細節被直接帶入。 +- 只有觸發詞有效,其他能力明顯下降。 +- 訓練損失下降,但未見過的情境表現變差。 + +調低學習率、步數或 LoRA 權重可能有幫助,但不能取代資料檢查。課堂必做是理解比較方法,不要求每位學生消耗免費 GPU 配額完成訓練。 + +## CPU 必做:微調企畫與模擬比較 + +1. 選擇完全合成物件或自己拍攝、可合法使用的素材。 +2. 規劃 12–20 張候選圖,實際只需提交縮圖與資料表。 +3. 把資料分為訓練候選與測試提示,不讓測試只重複訓練構圖。 +4. 使用教師提供的三組預先生成結果,判斷欠擬合、適當與過擬合。 +5. 寫下你會調整的下一個變因與理由。 + +## Colab 選配 + +若免費 GPU 可用,教師可示範低解析、小資料與短步數訓練。開始前先確認執行階段,結束後刪除不必要的大檔並中斷連線。Colab 不保證特定 GPU、可用時間或長期免費額度,因此不列為必做評分。 + +## 分流任務 + +### 日間引導 + +使用虛構圖示角色與固定資料卡,找出重複背景、角度不足、錯誤標註與授權不明四類問題。 + +### 進修挑戰 + +自訂一個與工作或興趣相關、但不涉及未同意人物的概念,提出基準提示、測試矩陣與停止條件。 + +## Exit Ticket + +1. LoRA 為什麼稱為「低秩」更新? +2. 小檔案是否表示沒有著作權與隱私風險? +3. 如何用未參與訓練的提示判斷泛化? + +## 主要來源 + +- [LoRA: Low-Rank Adaptation of Large Language Models](https://arxiv.org/abs/2106.09685) +- [Google Colab FAQ](https://research.google.com/colaboratory/faq.html) + + diff --git "a/course-115/weeks/W07_\347\224\237\346\210\220\345\275\261\345\203\217\345\267\245\344\275\234\345\256\244.md" "b/course-115/weeks/W07_\347\224\237\346\210\220\345\275\261\345\203\217\345\267\245\344\275\234\345\256\244.md" new file mode 100644 index 0000000..79c6cbf --- /dev/null +++ "b/course-115/weeks/W07_\347\224\237\346\210\220\345\275\261\345\203\217\345\267\245\344\275\234\345\256\244.md" @@ -0,0 +1,72 @@ +# W07|生成影像工作室:一致性比單張漂亮更重要 + +> 本週任務:製作 3–6 張可用於短影音的影像故事組,保留每次生成與後製紀錄。 + +![一致影像系列的製作流程](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/image-story-workflow.png) + +## 先定義視覺系統 + +單張好看的圖片未必能組成影片。開始生成前,先寫一頁視覺規格: + +- **主題句**:觀眾看完要記得什麼? +- **角色/物件**:外觀中不可改變的三項特徵。 +- **色盤**:主色、輔色與背景色。 +- **構圖**:景別、視角、主體位置與字幕安全區。 +- **媒材**:攝影、紙雕、扁平插畫或其他一致語言。 +- **禁用條件**:品牌、可辨識真人、浮水印、錯誤文字等。 + +## 一致性從控制變因開始 + +生成服務未必提供相同 seed 或模型選項,因此把可控制項分成三類: + +| 類型 | 例子 | 紀錄方式 | +|---|---|---| +| 固定 | 角色描述、比例、色盤、光線 | 視覺規格表 | +| 每張改變 | 動作、鏡位、場景事件 | 分鏡欄位 | +| 工具決定 | 模型版本、內部 seed、審查 | 工具與日期 | + +不要用「差不多」判斷一致性。可以檢查角色輪廓、顏色、道具、視線方向、光源、背景複雜度與字幕空間。 + +## 生成後仍要做影像處理 + +AI 輸出是素材,不是成品。用 Pillow 或 OpenCV 完成: + +1. 統一尺寸與比例,避免直接拉伸人物。 +2. 以裁切或補邊保留主體與字幕安全區。 +3. 調整亮度、對比或色調,但保留原始檔。 +4. 檢查手指、文字、反射、透視與不合理重複。 +5. 命名為 `scene01_v01.png`,不要只留下 `final_final2.png`。 + +## 課堂工作流程 + +```text +故事句 → 4 格分鏡 → 視覺規格 → 第一輪生成 → 瑕疵清單 + → 局部修正/重生 → 尺寸與色彩後製 → 來源表 +``` + +每組只選一個問題優先改善,例如角色一致性或字幕空間,不要無限制重抽圖片。 + +## 分流任務 + +### 日間引導 + +使用四格固定故事結構:「狀態—問題—處理—結果」。教師逐格檢查主體、構圖與安全區。 + +### 進修挑戰 + +自選敘事結構,建立可重複使用的提示元件;說明哪些一致性仍需後製或人工取捨。 + +## 繳交 + +- 3–6 張統一比例的影像。 +- 一頁視覺規格與分鏡。 +- 生成紀錄:工具、日期、提示、可見參數、採用/拒絕原因。 +- 瑕疵檢查與素材來源表。 + +## Exit Ticket + +1. 你固定了哪些變因? +2. 哪一項一致性最難靠提示控制? +3. 哪張圖若直接發布最可能造成誤解?你如何處理? + + diff --git "a/course-115/weeks/W08_\346\234\237\344\270\255\345\275\261\345\203\217\345\261\225\350\210\207\346\212\200\350\241\223\347\255\224\350\276\257.md" "b/course-115/weeks/W08_\346\234\237\344\270\255\345\275\261\345\203\217\345\261\225\350\210\207\346\212\200\350\241\223\347\255\224\350\276\257.md" new file mode 100644 index 0000000..f54fdb4 --- /dev/null +++ "b/course-115/weeks/W08_\346\234\237\344\270\255\345\275\261\345\203\217\345\261\225\350\210\207\346\212\200\350\241\223\347\255\224\350\276\257.md" @@ -0,0 +1,58 @@ +# W08|期中影像展:把「我覺得好看」變成可說明的技術判斷 + +> 本週任務:展示影像故事組,完成 3 分鐘說明與個人技術問答。此週可作為期中成績。 + +![影像作品展示與技術答辯](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/image-showcase.png) + +## 展示內容 + +每組或每位學生準備: + +1. **30 秒問題**:作品希望觀眾理解什麼? +2. **60 秒流程**:素材如何輸入、生成、後製與輸出? +3. **45 秒技術**:挑一個像素/卷積/擴散/潛在空間/LoRA 概念說明。 +4. **30 秒比較**:展示一個失敗版與改善依據。 +5. **15 秒限制**:現在仍不能解決什麼? + +## 不看華麗程度,看可驗證性 + +| 面向 | 檢核重點 | +|---|---| +| 媒體概念 | 格式、尺寸、色彩、濾鏡或生成參數說明正確 | +| 控制實驗 | 至少有一次只改一個變因的比較 | +| 故事與一致性 | 圖片能形成清楚順序,角色與視覺規格合理 | +| AI 與來源紀錄 | 提示、工具、日期、來源、同意與揭露完整 | +| 個人說明 | 能回答自己負責的流程、參數、失敗與限制 | + +## 可能的口頭問題 + +- 解析度與檔案大小為何不一定成正比? +- 卷積核中的負權重可能在做什麼? +- 擴散模型訓練與生成的方向有何不同? +- 潛在擴散為什麼比較省計算? +- 固定 seed 的比較為何仍可能跨平台不一致? +- LoRA 改變原模型的方式與完整微調有何差別? + +問答不是抓背誦,而是確認你能把作品與技術連起來。答不確定時,可以先指出輸入、觀察與需要查證的部分。 + +## 同儕回饋 + +每位觀眾只寫三項: + +- 我看懂的核心訊息。 +- 一個有證據的優點。 +- 一個可操作的下一步。 + +避免「很好看」「再豐富一點」等無法執行的語句。 + +## 本週繳交 + +- W07 全部素材與紀錄。 +- 期中展示頁或 3–5 頁簡報。 +- 個人 150–250 字反思:我的判斷依據如何改變? + +## 課後銜接 + +請把最適合期末短影音的 3–6 張圖放入專題素材資料夾。W09 不授新課,W10 從聲音開始。 + + diff --git "a/course-115/weeks/W09_\346\234\237\344\270\255\350\200\203\351\200\261.md" "b/course-115/weeks/W09_\346\234\237\344\270\255\350\200\203\351\200\261.md" new file mode 100644 index 0000000..bcefabb --- /dev/null +++ "b/course-115/weeks/W09_\346\234\237\344\270\255\350\200\203\351\200\261.md" @@ -0,0 +1,17 @@ +# W09|期中考週:不授新課 + +本週配合期中考週,不安排新內容、不新增實作進度。 + +## 學生確認事項 + +- 依班級首頁公告確認是否有考試、補交或個別說明時段。 +- 檢查 W01–W08 的 HackMD 筆記、程式、影像作品與 AI 使用紀錄是否可開啟。 +- 不要求趁本週提前製作後半學期作品;請保留休息與其他科目準備時間。 + +## 可選整理,不列新作業 + +若有餘裕,可把期中回饋分為「保留、修改、尚待查證」三欄,作為期末短影音的素材篩選依據。 + +下週將進入聲音模組:請準備耳機。需要錄音的活動只使用自己的聲音,或教師提供、具有授權的範例聲音。 + + diff --git "a/course-115/weeks/W10_\346\225\270\344\275\215\350\201\262\351\237\263\350\210\207\351\240\273\350\255\234.md" "b/course-115/weeks/W10_\346\225\270\344\275\215\350\201\262\351\237\263\350\210\207\351\240\273\350\255\234.md" new file mode 100644 index 0000000..3840601 --- /dev/null +++ "b/course-115/weeks/W10_\346\225\270\344\275\215\350\201\262\351\237\263\350\210\207\351\240\273\350\255\234.md" @@ -0,0 +1,73 @@ +# W10|聲音如何變成可以運算的數字? + +> 本週任務:錄製或使用授權聲音,觀察波形、取樣率、FFT 與 STFT。 + +![波形、取樣與時頻圖的關係](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/audio-sampling-stft.png) + +## 聲音不是一條漂亮的波浪線 + +麥克風將空氣壓力變化轉成電訊號,類比數位轉換器再依固定時間間隔取樣。兩個核心參數是: + +- **取樣率**:每秒記錄幾個樣本,例如 16 kHz 或 44.1 kHz。 +- **位元深度**:每個樣本可用多少離散數值表示振幅。 + +依 Nyquist–Shannon 取樣觀念,若要在理想條件下表示最高頻率 `f_max`,取樣率需高於 `2f_max`,而且實際系統還需要抗混疊濾波。這不是「超過兩倍就一定完美」,而是避免頻率折返的基本條件。 + +未壓縮單聲道 PCM 的資料量可估算為: + +```text +秒數 × 取樣率 × 位元深度 ÷ 8 +``` + +立體聲還要乘以聲道數。 + +## FFT 與 STFT 看見不同問題 + +- **波形**:振幅如何隨時間變化,適合看停頓、爆音與剪輯位置。 +- **FFT**:把整段訊號分解成頻率成分,但整段的時間位置被混在一起。 +- **STFT**:把訊號切成重疊短窗,每一窗做 FFT,得到時間—頻率表示。 + +窗長是一項取捨:較長的窗通常提供較好的頻率解析,較短的窗通常提供較好的時間解析。hop length 決定相鄰窗移動距離,也會影響圖的密度與運算量。 + +## CPU 實驗 + +執行 `labs/w10_audio_spectrum.py`: + +1. 載入 3–5 秒 WAV。 +2. 顯示取樣率、秒數與樣本數。 +3. 畫波形與 STFT dB 圖。 +4. 改變 `n_fft` 或 `hop_length`,只改一項並比較。 + +```python +import librosa +import numpy as np + +y, sr = librosa.load("input.wav", sr=None, mono=True) +D = librosa.stft(y, n_fft=1024, hop_length=256) +db = librosa.amplitude_to_db(np.abs(D), ref=np.max) +print(sr, len(y) / sr) +``` + +## 觀察任務 + +用拍手、持續母音與環境聲各做一個短樣本。拍手在時間上短、頻率分布較廣;持續母音有較穩定的週期與共振峰;環境聲則視來源而定。不要只把圖「貼上」,要用時間與頻率描述觀察。 + +### 日間引導 + +使用教師提供的三段音檔與固定參數,比對哪一段是拍手、母音與背景噪音。 + +### 進修挑戰 + +自選一段可授權錄音,提出一個需要 STFT 而不是只看 FFT 才容易回答的問題。 + +## Exit Ticket + +1. 44.1 kHz 是否代表人會聽到 44.1 kHz? +2. STFT 為什麼需要分窗? +3. 窗長增加時,時間與頻率解析通常如何取捨? + +## 延伸來源 + +- [librosa 文件](https://librosa.org/doc/latest/) + + diff --git "a/course-115/weeks/W11_\350\201\262\351\237\263\345\211\252\350\274\257\350\210\207\350\250\212\350\231\237\350\231\225\347\220\206.md" "b/course-115/weeks/W11_\350\201\262\351\237\263\345\211\252\350\274\257\350\210\207\350\250\212\350\231\237\350\231\225\347\220\206.md" new file mode 100644 index 0000000..53a094d --- /dev/null +++ "b/course-115/weeks/W11_\350\201\262\351\237\263\345\211\252\350\274\257\350\210\207\350\250\212\350\231\237\350\231\225\347\220\206.md" @@ -0,0 +1,67 @@ +# W11|剪輯聲音時,實際改變了什麼? + +> 本週任務:把一段 20–30 秒錄音剪成清楚、音量合理、可放進短影音的旁白。 + +![聲音剪輯與訊號處理流程](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/audio-editing.png) + +## 常見操作背後的訊號變化 + +- **裁切**:改變時間範圍,不應誤傷字尾或呼吸銜接。 +- **淡入/淡出**:以隨時間變化的增益乘上樣本,避免突然切斷造成喀聲。 +- **正規化**:依峰值或響度尺度調整整體增益;不等於壓縮動態範圍。 +- **動態壓縮**:縮小大聲與小聲之間差距,涉及 threshold、ratio、attack、release。 +- **濾波**:讓特定頻率範圍通過或衰減;過度降噪可能產生金屬或水聲感。 +- **time stretch**:改變長度而盡量維持音高。 +- **pitch shift**:改變音高而盡量維持長度。 + +傳統直接加快播放會同時縮短時間並提高音高。現代 time stretching 常利用相位聲碼器或其他方法分離兩者,但極端參數仍會產生瑕疵。 + +## 剪輯順序不是固定答案 + +一個可用起點是: + +```text +備份原音 → 裁切與重排 → 去除明顯低頻/噪音 → 音量與動態調整 + → 淡入淡出 → 以目標格式輸出 → 耳機與喇叭複查 +``` + +若來源已失真,後處理無法真正還原不存在的細節。錄音距離、環境噪音與爆音控制通常比事後套更多濾鏡有效。 + +## CPU 實驗 + +使用 `labs/w11_audio_edit.py` 或 Audacity 完成 A/B 比較。程式路徑包含: + +```python +from pydub import AudioSegment + +audio = AudioSegment.from_file("narration.wav") +clip = audio[1000:9000] +clip = clip.fade_in(120).fade_out(180) +clip.export("narration_edit.wav", format="wav") +``` + +額外用 librosa 或 FFmpeg 完成一項變速/變調測試,記錄參數與瑕疵。不要用處理後音訊冒充真實錄音。 + +## 聆聽檢核 + +- 字句是否完整,停頓是否自然? +- 音量是否忽大忽小或削波? +- 降噪後是否出現水聲、金屬感? +- 旁白與背景音樂同時播放時,語音是否仍清楚? +- 輸出格式、取樣率與聲道是否符合影片流程? + +### 日間引導 + +使用固定旁白完成裁切、淡入淡出與峰值檢查,繳交原音/成品對照。 + +### 進修挑戰 + +建立可重複執行的批次處理腳本,並對不同比例的參數提出停止條件。 + +## Exit Ticket + +1. 正規化與動態壓縮有何不同? +2. 直接加快播放為什麼會提高音高? +3. 哪一種瑕疵應優先回到錄音階段解決? + + diff --git "a/course-115/weeks/W12_\350\252\236\351\237\263\345\220\210\346\210\220\350\210\207\346\267\261\345\201\275\350\276\250\350\255\230.md" "b/course-115/weeks/W12_\350\252\236\351\237\263\345\220\210\346\210\220\350\210\207\346\267\261\345\201\275\350\276\250\350\255\230.md" new file mode 100644 index 0000000..af21a48 --- /dev/null +++ "b/course-115/weeks/W12_\350\252\236\351\237\263\345\220\210\346\210\220\350\210\207\346\267\261\345\201\275\350\276\250\350\255\230.md" @@ -0,0 +1,74 @@ +# W12|聲音深偽如何生成,又如何辨識風險? + +> 本週任務:理解 TTS 與 voice conversion 管線,比較真實/合成範例,完成生成內容揭露卡。 + +![語音合成、聲紋條件與檢測流程](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/voice-synthesis-safety.png) + +## 先分清楚三種任務 + +- **TTS(文字轉語音)**:輸入文字,輸出語音。 +- **speaker-conditioned TTS/voice cloning**:額外以說話者表示控制音色或身分特徵。 +- **voice conversion**:輸入一段語音,保留部分語言內容或韻律,再轉換說話者特徵。 + +典型的神經 TTS 可簡化為: + +```text +文字/音素 → 聲學模型 → 梅爾頻譜 → vocoder → 波形 + ↑ + speaker embedding(選用) +``` + +梅爾頻譜是接近人類聽覺尺度的時頻表示;vocoder 將頻譜條件轉為時間波形。speaker embedding 則把說話者特徵壓縮成向量,讓模型能控制或遷移音色。不同系統可能使用端到端架構,圖示只是概念分工。 + +## 「像」不等於「是真的」 + +人耳可能注意下列線索:不自然停頓、呼吸、子音、情緒轉換、背景聲不連續與語意脈絡。但現代模型能改善這些瑕疵,單靠「聽起來怪」不能成為可靠鑑識方法。 + +比較可信的處理方式是多證據: + +1. 核對來源、發布帳號與原始檔。 +2. 尋找剪接與轉碼痕跡,但理解轉傳也會改變檔案。 +3. 比對情境、時間與其他獨立證據。 +4. 將偵測器視為線索,不把單一分數當作定論。 +5. 高風險情況改用已知聯絡管道再次確認本人。 + +## 課堂安全規範 + +本週只使用:自己的聲音、取得明確同意的聲音,或教師提供/完全合成的範例。禁止仿冒同學、教師、公眾人物或未同意者;禁止把生成聲音拿去做身分驗證、惡作劇、詐騙或令人誤認的發布。 + +所有作品必須明顯標示「AI 合成/轉換聲音」,並留下同意、工具、日期、文字、參數與後製紀錄。 + +## CPU 必做:盲聽與證據分級 + +教師提供數段授權音訊,學生先盲聽,再看波形與頻譜。對每段填寫: + +- 我的判斷與信心 0–100。 +- 支持判斷的可觀察線索。 +- 還需要哪一種來源證據。 +- 若判斷錯誤,可能造成什麼影響。 + +重點不是猜中率,而是避免把直覺包裝成確定事實。 + +## 選配生成 + +可用工具以完全合成角色或自己的聲音,產生一句 3–5 秒短句,立刻加上揭露標籤。若免費服務要求不合理的個資、聲紋註冊或授權,應停止使用,改做教師範例分析。 + +### 日間引導 + +完成固定音檔比較與 TTS 管線排序,口頭說明 mel spectrogram 與 vocoder 的分工。 + +### 進修挑戰 + +設計一個「收到可疑語音訊息」的查證流程,包含技術線索與非技術交叉驗證。 + +## Exit Ticket + +1. speaker embedding 是聲音檔,還是特徵表示? +2. vocoder 的輸入與輸出大致是什麼? +3. 為什麼不能用單一深偽偵測分數直接定罪? + +## 主要來源 + +- [Transfer Learning from Speaker Verification to Multispeaker Text-To-Speech Synthesis](https://papers.nips.cc/paper_files/paper/2018/hash/6832a7b24bc06775d02b7406880b93fc-Abstract.html) + + diff --git "a/course-115/weeks/W13_\347\237\255\345\275\261\351\237\263\350\205\263\346\234\254\350\210\207\346\225\270\344\275\215\345\275\261\347\211\207.md" "b/course-115/weeks/W13_\347\237\255\345\275\261\351\237\263\350\205\263\346\234\254\350\210\207\346\225\270\344\275\215\345\275\261\347\211\207.md" new file mode 100644 index 0000000..0332885 --- /dev/null +++ "b/course-115/weeks/W13_\347\237\255\345\275\261\351\237\263\350\205\263\346\234\254\350\210\207\346\225\270\344\275\215\345\275\261\347\211\207.md" @@ -0,0 +1,63 @@ +# W13|短影音不是把素材排在一起而已嗎? + +> 本週任務:完成 15–30 秒直式短影音的腳本、分鏡與時間預算。 + +![短影音從腳本到時間軸的流程](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/short-video-storyboard.png) + +## 影片是依時間顯示的媒體系統 + +影片由影格依時間順序播放,但「一秒幾張」不是唯一品質指標。 + +- **FPS/frame rate**:每秒顯示或編碼多少影格。 +- **解析度與比例**:本課程目標為 720×1280、9:16。 +- **timebase**:時間戳的計量基準,剪輯與同步時比單看影格編號更可靠。 +- **音訊取樣時脈**:聲音與畫面使用不同取樣方式,長片若處理不當可能逐漸不同步。 + +30 FPS 的 20 秒影片約有 600 個顯示時點,但壓縮影片通常不會把 600 張完整圖片逐張保存。 + +## 用時間預算寫腳本 + +15–30 秒只夠一個核心訊息。先配置時間,再寫句子: + +| 區段 | 建議時間 | 功能 | +|---|---:|---| +| Hook | 0–3 秒 | 提出問題或反差 | +| Context | 3–8 秒 | 讓觀眾知道情境 | +| Process | 8–20 秒 | 呈現 1–3 個關鍵步驟 | +| Result | 20–26 秒 | 給出結果或比較 | +| Close | 26–30 秒 | 結論、揭露或下一步 | + +旁白要實際朗讀計時。字數不是唯一因素,停頓、專有名詞與字幕可讀時間都會改變長度。 + +## 四格或六格分鏡 + +每格至少包含: + +- 開始與結束時間。 +- 畫面素材與動作。 +- 旁白。 +- 畫面文字/字幕。 +- 音效或背景音樂。 +- 來源與生成標記。 + +字幕不應貼邊,也不能只靠顏色區分角色。手機直式畫面要預留平台介面可能遮住的區域。 + +## 紙上同步實驗 + +把 12 秒影片畫成 12 格「每秒狀態」,再改為 24 FPS 的影格觀念。討論:敘事事件沒有增加時,影格數增加改變的是時間取樣密度,不是內容本身。 + +## 分流任務 + +### 日間引導 + +使用五段式時間模板與四格分鏡;教師逐項檢查訊息、旁白長度、字幕安全區與素材可取得性。 + +### 進修挑戰 + +可自訂敘事節奏,但必須提出時間預算、素材風險與低算力備援方案。 + +## 繳交與 Exit Ticket + +繳交一句核心訊息、15–30 秒旁白、4–6 格分鏡與素材清單。回答:若素材不足,你會縮小訊息還是增加生成量?為什麼? + + diff --git "a/course-115/weeks/W14_\347\224\237\346\210\220\345\205\247\345\256\271\350\210\207\347\264\240\346\235\220\346\272\257\346\272\220.md" "b/course-115/weeks/W14_\347\224\237\346\210\220\345\205\247\345\256\271\350\210\207\347\264\240\346\235\220\346\272\257\346\272\220.md" new file mode 100644 index 0000000..93d34a6 --- /dev/null +++ "b/course-115/weeks/W14_\347\224\237\346\210\220\345\205\247\345\256\271\350\210\207\347\264\240\346\235\220\346\272\257\346\272\220.md" @@ -0,0 +1,74 @@ +# W14|如何建立可追溯的多媒體素材庫? + +> 本週任務:完成影像、旁白、音效、字幕與來源紀錄,形成可直接進入剪輯的素材包。 + +![素材來源、生成、後製與輸出的溯源鏈](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/media-provenance.png) + +## 素材包是系統的輸入契約 + +剪輯卡住,常不是軟體不會用,而是素材名稱、尺寸、長度與授權混亂。本週要讓每個檔案都回答:它從哪裡來、經過什麼處理、能用在哪裡。 + +建議結構: + +```text +project/ +├─ source/ # 不覆寫的原始素材 +├─ image/ # 統一比例後的視覺素材 +├─ audio/ # 旁白、音效、音樂 +├─ subtitle/ # 字幕與文字稿 +├─ output/ # 草稿與成品 +├─ README.md +├─ AI_USAGE.md +└─ ASSET_LOG.csv +``` + +## 每個素材至少記錄什麼? + +| 欄位 | 說明 | +|---|---| +| id | 不隨檔名改變的識別碼 | +| filename | 實際檔名與副檔名 | +| type | image/voice/music/sfx/font | +| source | 自製、網址、生成工具或教師素材 | +| license_or_consent | 授權條件或同意紀錄位置 | +| generated | 是否全部或部分由 AI 生成 | +| edits | 裁切、濾鏡、降噪、轉碼等 | +| intended_use | 對應分鏡與時間範圍 | + +EXIF 等內嵌 metadata 可能在轉檔或平台上傳時消失,因此不能只依賴檔案內部欄位。外部 `ASSET_LOG.csv` 與原始檔備份仍然必要。 + +## 生成內容的最低揭露 + +- 使用哪個工具與日期。 +- 文字提示與送入的素材種類。 +- 可取得的模型/版本/seed/比例資訊。 +- 生成後做過哪些人工修改。 +- 哪些地方可能讓觀眾誤認為真實。 + +避免把未公開個資、他人聲音或授權不清的圖片送入外部服務。即使只在課堂展示,也需遵守同意範圍。 + +## 課堂驗收 + +同組交換素材包,對方在五分鐘內檢查: + +1. 能否找到每一格分鏡需要的檔案? +2. 圖片比例與檔名是否一致? +3. 旁白長度是否符合時間預算? +4. 是否有不明來源或缺少同意的素材? +5. 若某個雲端工具不可用,是否仍能完成最低作品? + +### 日間引導 + +使用固定資料夾與教師提供的 `ASSET_LOG.csv` 範本,完成 8–12 個素材欄位。 + +### 進修挑戰 + +額外建立版本規則與自動檢查腳本,找出缺檔、重複檔名或格式不符項目。 + +## Exit Ticket + +1. metadata 與 provenance 有何差別? +2. 為什麼原始素材不可直接覆寫? +3. 你的素材包中,哪一個項目若失去授權就必須替換? + + diff --git "a/course-115/weeks/W15_FFmpeg\350\210\207\347\237\255\345\275\261\351\237\263\345\220\210\346\210\220.md" "b/course-115/weeks/W15_FFmpeg\350\210\207\347\237\255\345\275\261\351\237\263\345\220\210\346\210\220.md" new file mode 100644 index 0000000..c73cc91 --- /dev/null +++ "b/course-115/weeks/W15_FFmpeg\350\210\207\347\237\255\345\275\261\351\237\263\345\220\210\346\210\220.md" @@ -0,0 +1,74 @@ +# W15|影片如何被合成與壓縮? + +> 本週任務:用 FFmpeg 或 MoviePy 合成 720×1280 的短影音粗剪版,確認聲畫同步與可播放性。 + +![影格、關鍵影格、GOP、編碼與容器](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/video-codec-gop.png) + +## 容器不是編碼器 + +- **container**:把影像流、聲音流、字幕與 metadata 放在一起,例如 MP4、WebM、MKV。 +- **codec**:定義如何編碼/解碼媒體,例如 H.264、H.265、VP9、AV1、AAC。 + +副檔名 `.mp4` 只告訴你容器,不能保證裡面一定是某個編碼。播放失敗時要查 stream 資訊,而不是只改副檔名。 + +## 為什麼影片不用每張都存完整圖? + +常見視訊編碼會利用: + +- **空間冗餘**:同一影格的鄰近區域常相似。 +- **時間冗餘**:相鄰影格通常只改變一部分。 +- **I-frame/keyframe**:可較獨立解碼的影格。 +- **P/B-frame**:參考前後影格描述變化。 +- **GOP**:一組影格的結構與關鍵影格間距。 + +關鍵影格更密集通常有利於尋找與錯誤恢復,但可能增加位元率。實際結果還取決於內容、編碼器與 preset。 + +## FFmpeg 基本檢查 + +```bash +ffprobe -hide_banner input.mp4 +``` + +把影片轉為直式 720×1280,維持比例並補邊的概念命令: + +```bash +ffmpeg -i input.mp4 -vf "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2" -c:v libx264 -c:a aac output.mp4 +``` + +不要把不同比例的畫面直接拉伸到 9:16。`scale` 先維持比例,`pad` 再補足畫布。 + +## MoviePy 合成路徑 + +`labs/w15_make_short.py` 會讀取圖片清單、旁白與字幕,建立時間軸。學生需要修改的是素材路徑、每張停留時間與字幕,不必從零背誦整套 API。 + +完成後用 `ffprobe` 或播放器檢查: + +- 720×1280、預期 FPS、H.264/AAC(依課堂環境)。 +- 時長 15–30 秒。 +- 聲音沒有提早結束或逐漸不同步。 +- 字幕沒有超出安全區。 +- 檔案在另一台電腦也能播放。 + +## 位元率與品質 + +位元率是每秒分配的資料量。相同位元率下,高動態、細節密集的內容通常更難壓縮。CRF 模式傾向以品質目標調整位元率;固定 bitrate 則較容易控制傳輸規模。沒有一個參數能對所有內容最佳。 + +### 日間引導 + +使用固定四張圖與一段旁白完成合成,再比較兩個 CRF 設定的大小與瑕疵。 + +### 進修挑戰 + +依自選分鏡建立可重跑的組裝腳本,並保留一條 CPU 可完成的降級路徑。 + +## Exit Ticket + +1. MP4 與 H.264 為何不能互換使用? +2. 關鍵影格越多一定越好嗎? +3. 為何不能直接把 4:3 圖片拉伸成 9:16? + +## 延伸來源 + +- [FFmpeg 官方文件](https://ffmpeg.org/documentation.html) + + diff --git "a/course-115/weeks/W16_\351\200\262\345\272\246\345\260\217\345\240\261\345\221\212\350\210\207\347\263\273\347\265\261\346\270\254\350\251\246.md" "b/course-115/weeks/W16_\351\200\262\345\272\246\345\260\217\345\240\261\345\221\212\350\210\207\347\263\273\347\265\261\346\270\254\350\251\246.md" new file mode 100644 index 0000000..4f9d34d --- /dev/null +++ "b/course-115/weeks/W16_\351\200\262\345\272\246\345\260\217\345\240\261\345\221\212\350\210\207\347\263\273\347\265\261\346\270\254\350\251\246.md" @@ -0,0 +1,59 @@ +# W16|進度小報告:作品是否已經成為一個系統? + +> 本週任務:展示可播放的 rough cut、系統架構圖與風險清單,取得期末前最後一輪方向性回饋。 + +![短影音系統的輸入、處理、測試與輸出](https://raw.githubusercontent.com/willismax/MediaSystem-Python-Course/course-115-complete-materials/course-115/assets/diagrams/final-project-pipeline.png) + +## 五分鐘小報告 + +1. **30 秒**:作品要解決的溝通問題。 +2. **60 秒**:播放 rough cut;可有瑕疵,但必須能從頭播到尾。 +3. **60 秒**:系統架構與素材流向。 +4. **60 秒**:一個技術比較或失敗案例。 +5. **45 秒**:授權、同意與 AI 揭露。 +6. **45 秒**:剩餘風險、負責人與完成期限。 + +## 最小架構圖 + +架構圖至少呈現: + +```text +圖片來源 ─→ 尺寸/影像處理 ─┐ +旁白來源 ─→ 剪輯/音量處理 ─┼→ 時間軸/字幕 → 編碼輸出 → 播放測試 +文字腳本 ─→ 字幕檔 ─────────┘ +``` + +如果使用生成服務,把它放在對應素材來源或處理節點,並標出失敗時的替代路徑。 + +## 三類測試 + +| 類型 | 最低測試 | +|---|---| +| 正常 | 所有素材存在,完成預期輸出 | +| 邊界 | 旁白略長、圖片尺寸不同、字幕接近安全區 | +| 錯誤 | 缺檔、格式不支援、路徑錯誤或雲端服務不可用 | + +錯誤處理不一定要做圖形介面。能顯示清楚訊息、停止在合理位置、保留原始素材,就是有價值的系統行為。 + +## 回饋優先順序 + +1. 先修「不能播放、看不懂、聽不清楚」。 +2. 再修來源、同意、生成揭露與可重現性。 +3. 最後才增加轉場、動畫與裝飾。 + +### 日間引導 + +教師依固定檢核表逐組確認可播放、聲畫、字幕、來源與技術說明。 + +### 進修挑戰 + +除共同檢核外,說明你刪除或延後的一項功能,以及這個取捨如何降低風險。 + +## 本週繳交 + +- Rough cut MP4。 +- 一頁架構圖。 +- 三類測試紀錄。 +- 剩餘任務表與素材/AI 紀錄現況。 + + diff --git "a/course-115/weeks/W17_\346\234\237\346\234\253\346\210\220\346\236\234\347\231\274\350\241\250A.md" "b/course-115/weeks/W17_\346\234\237\346\234\253\346\210\220\346\236\234\347\231\274\350\241\250A.md" new file mode 100644 index 0000000..8820563 --- /dev/null +++ "b/course-115/weeks/W17_\346\234\237\346\234\253\346\210\220\346\236\234\347\231\274\350\241\250A.md" @@ -0,0 +1,32 @@ +# W17|期末成果發表 A + +> 本週不新增技術內容。重點是完整播放、個別說明、同儕回饋與可追溯紀錄。 + +## 每組發表建議 + +- 播放作品:30 秒內。 +- 問題、觀眾與敘事選擇:1 分鐘。 +- 影像、聲音與影片管線:2 分鐘。 +- 技術比較與失敗案例:1 分鐘。 +- 授權、同意、AI 揭露與限制:1 分鐘。 +- 個人口頭問答:依組員人數調整。 + +## 個別問答範圍 + +每位學生至少回答一個作品流程問題與一個原理問題,例如: + +- 這張影像經過哪些像素或格式處理? +- 生成影像中的 seed、guidance 或 LoRA 影響什麼? +- 旁白的取樣率、STFT 或音量處理如何檢查? +- 影片的 container、codec、FPS 與 bitrate 如何選擇? +- 哪個失敗案例改變了你的系統設計? + +## 同儕評閱 + +每位學生選兩組,記錄:核心訊息、可驗證的技術優點、一項風險與一個可執行改善。評閱不是人氣投票,不以生成量或特效數量判定。 + +## 發表後 + +W17 已發表者依回饋修正封存資料;未發表者不得把別組答辯內容直接當成自己的說明,仍需呈現實際決策與紀錄。 + + diff --git "a/course-115/weeks/W18_\346\234\237\346\234\253\346\210\220\346\236\234\347\231\274\350\241\250B\350\210\207\345\260\201\345\255\230.md" "b/course-115/weeks/W18_\346\234\237\346\234\253\346\210\220\346\236\234\347\231\274\350\241\250B\350\210\207\345\260\201\345\255\230.md" new file mode 100644 index 0000000..47eba73 --- /dev/null +++ "b/course-115/weeks/W18_\346\234\237\346\234\253\346\210\220\346\236\234\347\231\274\350\241\250B\350\210\207\345\260\201\345\255\230.md" @@ -0,0 +1,41 @@ +# W18|期末成果發表 B:把作品留下來,也把判斷留下來 + +> 本週完成第二梯次發表、課程回顧與專題封存。 + +## 發表規格 + +流程與 W17 相同:完整播放、系統管線、技術比較、失敗案例、來源/AI 揭露與個人口頭問答。 + +## 最終封存清單 + +```text +final-project/ +├─ output/final.mp4 +├─ source/ # 原始素材或來源清單 +├─ image/ +├─ audio/ +├─ subtitle/ +├─ code/ # 程式或可重現步驟 +├─ README.md +├─ AI_USAGE.md +├─ ASSET_LOG.csv +└─ REFLECTION.md +``` + +`README.md` 至少包含問題、目標觀眾、系統架構、執行方式、關鍵參數、測試、已知限制與組員分工。連結必須讓教師可在權限範圍內開啟。 + +## 個人反思 + +請用具體例子回答: + +1. 哪個媒體概念改變了你的作品決策? +2. 哪次 AI 建議被你拒絕,為什麼? +3. 哪個錯誤是靠測試而不是靠直覺發現? +4. 若只有 CPU 與 8 GB RAM,你如何維持最低可行作品? +5. 作品若要公開,還要補做哪一項授權、揭露或可及性檢查? + +## 課程結束不等於服務永久可用 + +雲端工具、免費額度與分享網址都可能失效。真正值得帶走的是:原始素材、開放格式的紀錄、可重現步驟、來源與同意證據,以及你能說明的技術判斷。 + +