
Google 從 Chrome 138 版本開始將 Gemini Nano 模型直接嵌入桌面客戶端。這是一種輕量級的大語言模型,在使用者裝置本地執行,所有資料處理都在端側完成,無需上傳至伺服器。瀏覽器會自動管理模型的分發、更新,並充分利用裝置的 GPU、NPU 或 CPU 進行硬體加速。
準備
軟體
支持 AI 功能的 Chrome 需要 138 版本以上。
瀏覽器相容性:
硬體
語言偵測器和翻譯器 API 適用於 Chrome 電腦版。這些 API 無法在行動裝置上運作。在 Chrome 中,只要符合下列條件,即可使用 Prompt API、Summarizer API、Writer API、Rewriter API 和 Proofreader API:
- 作業系統:Windows 10 或 11;macOS 13 以上版本 (Ventura 以上版本); Linux;或 Chromebook Plus 裝置上的 ChromeOS (從 Platform 16389.0.0 以上版本開始)。 使用 Gemini Nano 的 API 目前不支援 Android 版、iOS 版 Chrome,以及非 Chromebook Plus 裝置上的 ChromeOS。
- 儲存空間:包含 Chrome 設定檔的磁碟區至少要有 22 GB 的可用空間。
- GPU 或 CPU:內建模型可透過 GPU 或 CPU 執行。
- GPU:VRAM 必須超過 4 GB。
- CPU:RAM 16 GB 以上,CPU 核心 4 個以上。
- 網路:無限量數據或不計量的連線,下載模型會消耗大量數據。
啟用
-
設裝置型號優化為 Enabled
chrome://flags/#optimization-guide-on-device-model
-
設 Gemini Nano 提示 API 為 Enabled
chrome://flags/#prompt-api-for-gemini-nano
複製到地址欄打開。
狀態
Language Detector: Disable
Translator: Disable
Summarizer: Disable
語言偵測 Language Detector
<textarea id="language-detector-text">Guten Tag, mein Name ist Bexon Bai. Ich bin Mobile-Entwickle und spezialisiere mich auf Android- und iOS-Technologie.</textarea>
<button onclick="detection()">檢測</button>
模型狀態:
<textarea id="language-detector-result" disabled style="margin-top: 8px;display:none"></textarea>
譯者 Translator
<textarea id="translator-text">Guten Tag, mein Name ist Bexon Bai. Ich bin Mobile-Entwickle und spezialisiere mich auf Android- und iOS-Technologie.</textarea>
<select id="source-language"></select>
→
<select id="target-language"></select>
<button onclick="translation()">翻譯</button>
模型狀態:
<textarea id="translator-result" style="margin-top: 8px;"></textarea>
摘要 Summarizer
截止至 Chrome 138 版本,僅支持 [en, es, ja]
<textarea id="summarizer-text" style="margin-top: 8px;"></textarea>
<select id="summarizer-type">
<option value="key-points">Key Points</option>
<option value="tldr">TL;DR</option>
<option value="teaser">Teaser</option>
<option value="headline" selected="">Headline</option>
</select>
<select id="summarizer-length">
<option value="short" selected="">Short</option>
<option value="medium">Medium</option>
<option value="long">Long</option>
</select>
<select id="summarizer-format">
<option value="markdown" selected="">Markdown</option>
<option value="plain-text">Plain text</option>
</select>
<select id="summarizer-language">
<option value="en" selected="">English</option>
<option value="es">Spanish</option>
<option value="ja">Japanese</option>
</select>
<button onclick="summaryGeneration()">摘要生成</button>
模型狀態:
<textarea id="summarizer-result" style="margin-top: 8px;"></textarea>
技術文檔
<script language="javascript">
const languages = {"ar":"Arabic","bg":"Bulgarian","bn":"Bengali","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-US":"English (United States)","en-GB":"English (United Kingdom)","en-CA":"English (Canada)","en-AU":"English (Australia)","en-IN":"English (India)","es":"Spanish","es-ES":"Spanish (Spain)","es-MX":"Spanish (Mexico)","es-AR":"Spanish (Argentina)","es-CO":"Spanish (Colombia)","fi":"Finnish","fr":"French","fr-FR":"French (France)","fr-CA":"French (Canada)","fr-CH":"French (Switzerland)","fr-BE":"French (Belgium)","he":"Hebrew","hi":"Hindi","hr":"Croatian","hu":"Hungarian","id":"Indonesian","it":"Italian","it-CH":"Italian (Switzerland)","ja":"Japanese","kn":"Kannada","ko":"Korean","lt":"Lithuanian","mr":"Marathi","nl":"Dutch","nl-BE":"Dutch (Belgium)","no":"Norwegian","pl":"Polish","pt":"Portuguese","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro":"Romanian","ru":"Russian","sk":"Slovak","sl":"Slovenian","sv":"Swedish","ta":"Tamil","te":"Telugu","th":"Thai","tr":"Turkish","uk":"Ukrainian","vi":"Vietnamese","zh":"Chinese","zh-Hans":"Chinese (Simplified)","zh-Hant":"Chinese (Traditional)","zh-CN":"Chinese (China)","zh-HK":"Chinese (Hong Kong)","zh-MO":"Chinese (Macao)","zh-SG":"Chinese (Singapore)","zh-TW":"Chinese (Taiwan)"}
document.getElementById('browser-ua').innerText = navigator.userAgent
Object.assign(document.getElementById('browser-ua').style, {
whiteSpace: 'pre-wrap',
wordWrap: 'break-word'
});
function isAIOnChromeSupported() {
return 'LanguageDetector' in self
}
if (isAIOnChromeSupported()) {
document.getElementById('browser-ai-compatibility').innerText = "可用"
document.getElementById('browser-ai-compatibility').style.color = 'green'
} else {
document.getElementById('browser-ai-compatibility').innerText = "不可用"
document.getElementById('browser-ai-compatibility').style.color = 'red'
}
window.onload = function(e) {
console.log("Translator", 'Translator' in self)
if ('Translator' in self) {
document.getElementsByClassName('translator')[0].innerText = 'Enabled'
document.getElementsByClassName('translator')[0].style.color = 'green'
} else {
document.getElementsByClassName('translator')[0].innerText = 'Disable'
document.getElementsByClassName('translator')[0].style.color = 'red'
}
console.log("Language Detector", 'LanguageDetector' in self)
if ('LanguageDetector' in self) {
document.getElementsByClassName('language-detector')[0].innerText = 'Enabled'
document.getElementsByClassName('language-detector')[0].style.color = 'green'
} else {
document.getElementsByClassName('language-detector')[0].innerText = 'Disable'
document.getElementsByClassName('language-detector')[0].style.color = 'red'
}
if ('Summarizer' in self) {
document.getElementsByClassName('summarizer')[0].innerText = 'Enabled'
document.getElementsByClassName('summarizer')[0].style.color = 'green'
}
if ('LanguageDetector' in self) {
checkLanguageDetector()
}
perTranslate()
initSummarizer()
}
function initSummarizer() {
var demoText =
`Extracting WARP to generate a WireGuard
Aug 23, 2023 develop
💡 If you just want to use WARP, just install the [official client 1.1.1.1](https://1.1.1.1) directly
⚠️ Since my computer is a MacBook, this post is based on macOS, if you are using Windows or Linux, some of the steps may be different.
Preparatory steps:
Install the official client
Use wgcf to convert to a WireGuard profile
Settings WireGuard client
Other: Find endpoints in CN 🇨🇳
Install the official client
Client download: 🔗 Link
Use wgcf to convert to a WireGuard profile
Install a warp unofficial, cross-platform CLI wgcf
wgcf project:
https://github.com/ViRb3/wgcf
I’m using the Mac, so just use 🍺 Homebrew to install it.
brew install wgcf
Use cd to enter the directory where you want to store the config file (in case you can’t find it😞
Register wgcf
wgcf register
The command mentioned above will generate a wgcf-account.toml file in the current directory.
Run the following command in a terminal:
wgcf generate
The WireGuard profile will be saved under wgcf-profile.conf.
Getting the License Key in CloudFlare WARP
⚙︎ → Preferences → Account → License Key
Untitled
Untitled
Edit wgcf-account.toml directly with the new license key and run:
wgcf update
or, using an environment variable:
WGCF_LICENSE_KEY="Your License Key" wgcf update
The license will be applied, and a new private key will be created for your account.
Okay, now you have all the setup content you need to use. ✅
Settings WireGuard client
open the wgcf-profile.conf file.
[Interface]
PrivateKey = ***
Address = 172.16.0.2/32
Address = 2606:4700:110:8804:f095:3189:b06c:f68f/128
DNS = 1.1.1.1
MTU = 1280
[Peer]
PublicKey = ***
AllowedIPs = 0.0.0.0/0
AllowedIPs = ::/0
Endpoint =
engage.cloudflareclient.com:2408
I set on Shadowrocket app
Untitled
Fill the text field with the values in wgcf-profile.conf
Done! 🚀
Other: Find endpoints in CN 🇨🇳
The official domain name used at CN has been affected by DNS pollution.
engage.cloudflareclient.com:2408
To access it properly, you need to find an endpoint.
Here we use MisakaNo’s method to find the Endpoint.
macOS/Linux:
Run this command
wget -N
https://gitlab.com/Misaka-blog/warp-script/-/raw/main/files/warp-yxip/warp-yxip.sh && bash
warp-yxip.sh
Windows:
Download this file
Unzip it (7zip)
Run warp-yxip.bat
You’ll get a list and a csv document for Endpoint, just pick the one that works.
Good luck 🍀
`
document.getElementById('summarizer-text').value = demoText
}
async function detection() {
console.log("Start detection.")
const detector = await LanguageDetector.create({
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(`Downloaded ${e.loaded * 100}%`);
document.getElementsByClassName('language-detector-status')[0].style.color = 'var(--minima-text-color)'
document.getElementsByClassName('language-detector-status')[0].innerText = `${e.loaded * 100}%`
});
},
});
const text = document.getElementById('language-detector-text').value
const results = await detector.detect(text);
console.log("Language detector results:", results)
document.getElementById('language-detector-result').style.display = 'block'
document.getElementById('language-detector-result').value = ''
for (const result of results) {
document.getElementById('language-detector-result').value += `language: ${result.detectedLanguage}, confidence: ${result.confidence * 100}%\n`
}
}
async function checkLanguageDetector() {
const state = await LanguageDetector.availability()
document.getElementsByClassName('language-detector-status')[0].innerText = state
console.log("Check language detector:", state)
if (state == 'available') {
document.getElementsByClassName('language-detector-status')[0].style.color = 'green'
} else {
document.getElementsByClassName('language-detector-status')[0].style.color = 'red'
}
}
async function translation() {
console.log("Start translate.")
const sourceLanguageValue = document.getElementById('source-language').value
const targetLanguageValue = document.getElementById('target-language').value
const targetLanguageLabel = document.getElementById('target-language').selectedOptions[0].text;
const translator = await Translator.create({
sourceLanguage: sourceLanguageValue,
targetLanguage: targetLanguageValue,
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(`Downloaded ${sourceLanguageValue} ${targetLanguageValue} ${e.loaded * 100}%`);
document.getElementsByClassName('translator-status')[0].style.color = 'var(--minima-text-color)'
document.getElementsByClassName('translator-status')[0].innerText = `Download ${targetLanguageLabel} ${e.loaded * 100}%`
});
},
});
const text = document.getElementById('translator-text').value
const result = await translator.translate(text);
document.getElementById('translator-result').value = result
}
async function perTranslate() {
Object.entries(languages).forEach(([code, name]) => {
if (code == 'de') {
document.getElementById('source-language').innerHTML += `<option value="${code}" selected>${name}</option>`
} else {
document.getElementById('source-language').innerHTML += `<option value="${code}">${name}</option>`
}
if (code == 'zh-Hant') {
document.getElementById('target-language').innerHTML += `<option value="${code}" selected>${name}</option>`
} else {
document.getElementById('target-language').innerHTML += `<option value="${code}">${name}</option>`
}
})
}
async function summaryGeneration() {
if ('Summarizer' in self == false) {
return;
}
const summarizerText = document.getElementById('summarizer-text').value
const summarizerType = document.getElementById('summarizer-type').value
const summarizerFormat = document.getElementById('summarizer-format').value
const summarizerLength = document.getElementById('summarizer-length').value
const summarizerLanguage = document.getElementById('summarizer-language').value
const options = {
type: summarizerType,
format: summarizerFormat,
length: summarizerLength,
outputLanguage: summarizerLanguage,
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(`Downloaded ${e.loaded * 100}%`);
document.getElementsByClassName('summarizer-status')[0].style.color = 'var(--minima-text-color)'
document.getElementsByClassName('summarizer-status')[0].innerText = `Download ${e.loaded * 100}%`
});
}
};
const availability = await Summarizer.availability();
if (availability === 'unavailable') {
return;
}
if (navigator.userActivation.isActive) {
document.getElementById('summarizer-result').placeholder = "生成中……"
const summarizer = await Summarizer.create(options);
const summary = await summarizer.summarize(summarizerText, { });
document.getElementById('summarizer-result').value = summary
console.log(`Summary ${summary}`);
document.getElementById('summarizer-result').placeholder = ""
}
}
</script>