2024年10月3日 星期四

體驗 Google Cloud AI Study Jam 2024: goo.gle/csj-tw-2024


看著認識的 Google 技術傳教士分享這活動,趁颱風天來體驗一下 XD 總算做完 Path 1 的學習歷程,微累,但至少可以拿到個貼紙禮物了?XD


共 25 份學習教材,完成時間約 48 小時。

在影片觀看上,可以用兩倍速吸收,旁邊又有中文字幕,很不錯。整體耗時是還可以接受,倒是透過這次了解 vertex ai 的操作介面,很佛的設定好 prompt 後,還可以匯出程式碼,這點很方便,若要說卡關的話,好像有某堂課要驗證 vertex ai studio 某操作項目時,一直失敗,後來我猜到了,他是在掃已儲存的 prompt 的比對方式是用英文作為判斷,這時要把 vertex ai studio 的操作資料改成英文 (examples) 才能被偵測成功完成該項目。

整體上就熟悉一下 Google Cloud 服務,很順,此外 Path 1 某堂課也有稍微改 code 的地方,對於工程師背景的人來說,看懂題目,很簡單就能完成的。印象中,有一題很卡,題目上只有一個 example ,要自己再想另一個填入,接著,還要再想額外一個當作 promot & Test Input 來實驗(總共想兩個新句子),大概這題一開始不順會瞬間喪失自信 XD 但撐過了就沒問題了。經過這痛苦後,也被訓練到,看題目時可以挑熟悉的語言(中文),但建議還要另開一頁用英文的再看一次,避免翻中文時,一些關鍵操作看不懂。特別是一些操作選單,英文清楚非常多。

之前一直想用 LangChain 也略知一二,但恰好教學內容提了簡單 Python Code 真不錯,醍醐灌頂。

過去已經約一年多都在用 OpenAI API 開發服務,這次體驗了 Google 牌,也才了解 PaLMGemini 和 Vertex AI 的不同,之前 PaLM 剛推出來時,同事整合會議記錄,但整體上還是有不少需要客製化的地方,很快就放棄整合,沒想到 Google NotebookLM 最近推出,狂勝,且根據 Path 1 的學習歷程,大概能體會 Google 在 Responsible AI 投入很驚人的資源。

2024年9月23日 星期一

PHP 開發筆記 - 在 macOS 15 安裝 PHP 7.4 開發環境 @ Apple M2 / arm64

原本是習慣用 MacPorts 管理套件,然後升上 macOS 15 後,跑了 MacPorts migration 後,發現 php74 不見了,細追才發現沒裝起來 XD 努力研究了一下:

--->  Configuring php74
Warning: Configuration logfiles contain indications of -Wimplicit-function-declaration; check that features were not accidentally disabled:
  gethostbyname_r: found in php-7.4.33/config.log
  _controlfp: found in php-7.4.33/config.log
  _controlfp_s: found in php-7.4.33/config.log
Warning: Configuration logfiles contain indications of -Wimplicit-int; check that features were not accidentally disabled:
  found in php-7.4.33/config.log
--->  Building php74
Error: Failed to build php74: command execution failed   
Error: See /opt/local/var/macports/logs/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_php/php74/main.log for details.
Error: Follow https://guide.macports.org/#project.tickets if you believe there is a bug.
Error: Processing of port php74 failed
--->  Some of the ports you installed have notes:
  db48 has the following notes:
    The Java and Tcl bindings are now provided by the db48-java and
    db48-tcl subports.

===

:info:build ld: warning: ignoring duplicate libraries: '-largon2', '-lxml2', '-lz'
:info:build Undefined symbols for architecture arm64:
:info:build   "_res_9_dn_expand", referenced from:
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       ...  
:info:build   "_res_9_dn_skipname", referenced from:
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build   "_res_9_init", referenced from:
:info:build       _zif_dns_check_record in dns.o
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build   "_res_9_search", referenced from:
:info:build       _zif_dns_check_record in dns.o
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build ld: symbol(s) not found for architecture arm64
:info:build clang: error: linker command failed with exit code 1 (use -v to see invocation)
:info:build make: *** [sapi/cli/php] Error 1
:info:build make: *** Waiting for unfinished jobs....
:info:build ld: warning: ignoring duplicate libraries: '-largon2', '-lxml2', '-lz'
:info:build Undefined symbols for architecture arm64:
:info:build   "_res_9_dn_expand", referenced from:
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       _php_parserr in dns.o
:info:build       ...  
:info:build   "_res_9_dn_skipname", referenced from:
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build   "_res_9_init", referenced from:
:info:build       _zif_dns_check_record in dns.o
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build   "_res_9_search", referenced from:
:info:build       _zif_dns_check_record in dns.o
:info:build       _zif_dns_get_record in dns.o
:info:build       _zif_dns_get_mx in dns.o
:info:build ld: symbol(s) not found for architecture arm64
:info:build clang: error: linker command failed with exit code 1 (use -v to see invocation)
:info:build make: *** [sapi/phpdbg/phpdbg] Error 1
:info:build make: Leaving directory `/opt/local/var/macports/build/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_php/php74/work/php-7.4.33'
:info:build Command failed:  cd "/opt/local/var/macports/build/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_php/php74/work/php-7.4.33" && /usr/bin/make -j8 -w all
:info:build Exit code: 2
:error:build Failed to build php74: command execution failed
:debug:build Error code: CHILDSTATUS 94983 2
:debug:build Backtrace: command execution failed
:debug:build     while executing
:debug:build "system {*}$notty {*}$callback {*}$nice $fullcmdstring"
:debug:build     invoked from within
:debug:build "command_exec -callback portprogress::target_progress_callback build"
:debug:build     (procedure "portbuild::build_main" line 10)
:debug:build     invoked from within
:debug:build "$procedure $targetname"
:error:build See /opt/local/var/macports/logs/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_php/php74/main.log for details.

因此,主要是 _res_9_dn_* 等等的問題,問 AI 說,解方若還要繼續用 MacPorts 管理,只能裝個肥滋滋的 bind9

% sudo port install bind9
% sudo port clean php74
% sudo port install php74 configure.cflags="-I/opt/local/include" configure.ldflags="-L/opt/local/lib -lbind9 -ldns -lisc"

但這樣跑也失敗了

接著又試了 

% brew install php@7.4   
Warning: No available formula with the name "php@7.4". Did you mean php@8.2, php@8.1 or php@8.0?
==> Searching for similarly named formulae and casks...
==> Formulae
php@8.2                                               php@8.1                                               php@8.0

To install php@8.2, run:
  brew install php@8.2

雖然繼續深入問 AI 可以得到要做 php-7.4 patch 檔案,但這樣搞下去維護太累了,等於自己編譯 php74 跟改 code 了

最終,取個平衡,未來需要 php74 的環境,就靠 Docker 吧 XD 就單純這樣用即可:

% docker run -it -v ~/projects/:/projects -p 8080:8080 -p 8000:8000 -p 8443:8443 --rm php:7.4-cli bash

如此在 Docker 內部用 `php -S 0.0.0.0:8000` 就可以在 Host 端存取查看運行結果了

2024年9月8日 星期日

Dart 開發筆記 - 製作 Big5 to UTF8 工具以及發布到 pub.dev


最近想整理以前 C++ 寫過的東西來尋找熱情,先試試把很片段的小東西轉出來使用,這次純靠 claude.ai 做了滿多事,包括 README.md, CHANGELOG.md, example 和 test 都是他寫的 XD 剛好把最懶散的部分都給搞定了,因此 example 和 test 裡頭都會有簡體中文,甚至 README 一開始也給簡中版,只好請他給予英文版即可

整個過程大概不用兩個小時,非常舒服,花比較多的時間是請他把工具規劃成 library 的過程,claude ai 提了不少建議,但我也打槍他,最終有了現況的產出,這時真的感受到 AI 輔助的有趣的地方,還會提供一些觀點,像是 Flutter 他有多種平台,有些平台不適合 io 操作,這時會收到一些 AI 給予的建議,調整一些實作方向。


其中 pub.dev 發布過程還不太熟悉,參考官網 dart.dev.org.tw/tools/pub/publishing 先用指令:

```
big5_utf8_converter_dart % dart pub publish --dry-run                 
Resolving dependencies... 
Downloading packages... 
  _fe_analyzer_shared 73.0.0 (74.0.0 available)
  analyzer 6.8.0 (6.9.0 available)
  macros 0.1.2-main.4 (0.1.3-main.0 available)
Got dependencies!
3 packages have newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.
Publishing big5_utf8_converter 1.0.0 to https://pub.dev:
├── CHANGELOG.md (<1 KB)
├── LICENSE (1 KB)
├── README.md (2 KB)
├── assets
│   └── big5_to_utf8_lookup.bin (64 KB)
├── bin2dart.dart (<1 KB)
├── example
│   ├── big5_utf8_converter_example.dart (<1 KB)
│   └── big5_utf8_converter_load_table_example.dart (1 KB)
├── lib
│   ├── big5_utf8_converter.dart (<1 KB)
│   └── src
│       ├── big5_to_utf8_lookup_data.dart (260 KB)
│       └── big5_utf8_converter.dart (1 KB)
├── pubspec.yaml (<1 KB)
└── test
    └── big5_decoder_test.dart (2 KB)

Total compressed archive size: 98 KB.
The server may enforce additional checks.

Package has 0 warnings.
```

接著才使用 Github Actions 來做事,僅需使用 Github Actions 預設的 Dart 就會幫跑 test case:


```
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603

      - name: Install dependencies
        run: dart pub get

      - name: Analyze project source
        run: dart analyze

      - name: Run tests
        run: dart test
```

最後,更新 README.md 增加圖標令牌:

[![pub package](https://img.shields.io/pub/v/big5_utf8_converter.svg)](https://pub.dev/packages/big5_utf8_converter)
[![Build Status](https://github.com/changyy/big5_utf8_converter_dart/workflows/Dart/badge.svg)](https://github.com/changyy/big5_utf8_converter_dart/actions)

收工!

2024年8月28日 星期三

Xiaomi 小米智慧直流變頻電風扇 斷頭 維修


小米智慧直流電頻電風扇 斷

買了六年的電風扇,之前已經有點感覺有異狀,但在某一天早上,終於因為不當的使用方式移動他而斷掉了,查了一下討論文,沒想到還滿多人講這件事,包括沒有設計提拉他的把手 XD 這大概就是極簡設計吧,正確的移動他是要握住下方的桿子。

研究了一會兒,在 BiliBili 和抖音有看到“拆解”影片,再細看一下淘寶有賣零件!不錯,立馬研究一下,但想說零件來再說,結果...

拿到零件後,要進行最後的處理,才發現最困難的地方就是拆除斷掉的項目



因為這段是把塑膠套管透過強力膠黏住的,解方只有兩種,一種是加熱處理,另一種是設法先鋸成切片,各個片段取出時比較容易。

原本想說懶得買鋸子,就學影片火烤,但烤了老半天都看到塑膠管形變,就是拔不出來,最後在暴力處理的過程,正式把斷掉的部分拔掉,但管子內管還是沒有清乾淨,新買的零件也裝不下去,最終回去買鋸子鋸個兩半,在用一字跟夾子強力清除,不得不說,清除完有種爽感跟成就感,那是擠壓多時的恨意 XD







最後,安裝很順利,畢竟拆解時都有拍照,對著回顧一下就裝完了,裝完當下還覺得有點怪怪的,結果裝反了 XD 又拆一次再裝一次,熟到可以進工廠組裝了


這張是裝錯的,裝反了

終於,結束了這場鬧劇。

這邊善意提醒,購買零件時,請務必把電風扇底部的型號給予賣家,因為小米電風扇有很多款,每款格式不同,主要產品型號可以讓賣家快速確認,此外,可以的話,那還是放棄修復了 XD 年限到了,這類商品就自然地汰舊換新吧!



2024年8月23日 星期五

Python 開發筆記 - 不透過 Google API Key 下載公開 Google Sheets 資料,將每個 Sheet 匯出 csv 格式

有個工作任務要做 Google Sheets 資料比對,最簡單的方式就把他們匯出後用 git diff 來比對即可,想試著用 AI 產生一隻 python 小工具,只要輸入 Google Sheets URL 或是 Google Sheets URL 內關鍵的辨識 ID (在此稱作 spreadsheet id),就能夠下載該 Google Spreadsheet 內所有 sheet 資料

然後,要下載指定 sheet 必須得知每個 sheet gid ,這個問 ChatGPT-4o 或 Claude.ai 老半天還是沒法解,包過上傳 html static code,最後自己還是跳下來收尾人工刻一下,原理:

  • 先設法下載到 HTML Code
  • 透過 docs-sheet-tab-caption 抓出 Sheet Name
  • 透過 var bootstrapData = {...}; 得知內有 Sheet Name 與 Gid 的資料
  • 再用 [0,0,\"gid\",[ 格式,找到 gid
連續動作:

% python3 main.py 

usage: main.py [-h] (--google-spreadsheet-url GOOGLE_SPREADSHEET_URL | --google-spreadsheet-id GOOGLE_SPREADSHEET_ID) [--output OUTPUT]

main.py: error: one of the arguments --google-spreadsheet-url --google-spreadsheet-id is required


% python3 main.py --google-spreadsheet-id 'XXXXXXXXXXXXXXXXXXXXXXX'

[INFO] Downloaded sheet: sheet01 to sheets_csv/sheet01.csv

[INFO] Downloaded sheet: sheet02 to sheets_csv/sheet02.csv

[INFO] Downloaded sheet: sheet03 to sheets_csv/sheet03.csv

[INFO] Downloaded sheet: sheet04 to sheets_csv/sheet04.csv

[INFO] Downloaded sheet: sheet05 to sheets_csv/sheet05.csv


程式碼:

```
% cat main.py 
import argparse
import re
import requests
import json
import os

def extract_spreadsheet_id(url):
    match = re.search(r'/d/([a-zA-Z0-9-_]+)', url)
    return match.group(1) if match else None

def get_spreadsheet_info(spreadsheet_id):
    url = f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}/edit"
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    }
    
    try:
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        html_content = response.text
        
        # Extract sheet names
        sheet_names = re.findall(r'docs-sheet-tab-caption[^>]+>([^<]+)</div>', html_content)
        #print(f"Found sheet names: {sheet_names}")

        # Extract mergedConfig
        config_match = re.search(r'var bootstrapData\s*=\s*({.*?});', html_content, re.DOTALL)
        if config_match:
            config_str = config_match.group(1)
            sheet_info = {}
            try:
                for index, sheet_name in enumerate(sheet_names):
                    #print(f"Processing sheet: {sheet_name}, index: {index}")
                    beginPattern = f'[{index},0,\\"'
                    endPattern = f'\\",['
                    beginIndex = config_str.find(beginPattern)
                    endIndex = config_str.find(endPattern, beginIndex)
                    gidValue = config_str[beginIndex + len(beginPattern):endIndex]
                    sheet_info[sheet_name] = gidValue
                return sheet_info
            except Exception as e:
                print(f"[INFO] Error extracting sheet information: {e}")
                return None
        else:
            print("[INFO] Could not find bootstrapData in the HTML content")
            return None
    except requests.RequestException as e:
        print(f"[INFO] Error fetching the spreadsheet: {e}")
        return None

def download_sheet_as_csv(spreadsheet_id, sheet_name, gid, output_folder):
    csv_url = f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}/export?format=csv&gid={gid}"
    csv_response = requests.get(csv_url)
    
    if csv_response.status_code == 200:
        output_path = os.path.join(output_folder, f"{sheet_name}.csv")
        with open(output_path, 'wb') as f:
            f.write(csv_response.content)
        print(f"[INFO] Downloaded sheet: {sheet_name} to {output_path}")
    else:
        print(f"[INFO] Failed to download sheet: {sheet_name}. Status code: {csv_response.status_code}")

def main():
    parser = argparse.ArgumentParser(description="Extract Google Spreadsheet information")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--google-spreadsheet-url", help="Google Spreadsheet URL")
    group.add_argument("--google-spreadsheet-id", help="Google Spreadsheet ID")
    parser.add_argument('--output', type=str, default='sheets_csv', help='The directory to save the CSV files')
    args = parser.parse_args()

    if args.google_spreadsheet_url:
        spreadsheet_id = extract_spreadsheet_id(args.google_spreadsheet_url)
    else:
        spreadsheet_id = args.google_spreadsheet_id

    if not spreadsheet_id:
        print("[INFO] Invalid Google Spreadsheet URL or ID")
        return

    sheet_info = get_spreadsheet_info(spreadsheet_id)
    if sheet_info:
        for name, gid in sheet_info.items():
            download_sheet_as_csv(spreadsheet_id, name, gid, args.output)
    else:
        print("[INFO] Failed to extract sheet information")

if __name__ == "__main__":
    main()
```