動手作膽小狗英雄裡的嘴砲電腦

于正龍(Ricky)-avatar-img
發佈於人工智能 個房間
更新於 發佈於 閱讀時間約 27 分鐘
raw-image
raw-image

這部很老的卡通了 一直對膽小狗英雄頂樓那台電腦很有印象

毒舌 嘴砲 自大 嘲諷 但總是能給膽小狗有用的建議 幫助他解決各種問題

最近剛好比較多時間在寫程式 又看到現在LLM技術發展成熟

基本上可以實現跟卡通裡面一模一樣的效果了

整理一下功能

1.毒舌 嘴砲 自大 嘲諷 但總是能給膽小狗有用的建議 幫助他解決各種問題

2.有語音功能

3.有記憶功能

4.有一個簡單的介面

整理好了就開始規劃

我設計成3層式架構 前端 後端 資料庫 分離開來比較好維護 管理 跟擴充

raw-image

表現層就是UI介面設計我用html css javascript設計

業務邏輯層就是程式的運算跟邏輯 我使用 python flask

資料存取層就是放資料我用Postgres SQL儲存資料

部屬方式就是放在雲端RENDER上

raw-image

RENDER剛好有提供

1.Static Site服務

2.web service服務

3.Postgres SQL服務

剛好很適合作成這樣子的架構來部屬跟管理

+---------------------+
| 使用者 User |
+----------+----------+
|
| 操作/互動 (點擊/輸入)
v
+---------------------+
| 前端 Frontend |
| (HTML/CSS/JS) |
| - 發送HTTP請求 |
| - 更新畫面 |
+----------+----------+
|
| HTTP Request (API Call)
v
+---------------------+
| 後端 Backend |
| (Flask) |
| - 接收請求 |
| - 驗證/處理資料 |
| - 呼叫外部API |
| - 存取資料庫 |
+---+--------------+--+
| |
| |
| |
v v
+--------+ +----------------+
| 資料庫 | | 外部 API |
| PostgreSQL| | (第三方服務) |
+--------+ +----------------+

| ^
| |
+--------------+
回傳資料

前端資料夾目錄架構

raw-image

前端介面就這樣很簡單但好用

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Rooftop Computer - Courage Style</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="screen">
<h1>Courage's Rooftop AI</h1>
<div id="log"></div>
<input type="text" id="input" placeholder="Type your question here..." autofocus autocomplete="off" spellcheck="false" />
</div>
<script src="script.js"></script>
</body>
</html>
raw-image

📄 HTML 結構說明:

<head> 部分

  • <meta charset="UTF-8" />:設定字元編碼為 UTF-8,支援多種語言符號。
  • <title>:設定網頁標題為「Rooftop Computer - Courage Style」。
  • <link rel="stylesheet" href="style.css" />:引入外部 CSS 檔案,用來設計樣式。

<body> 部分

  • <div class="screen">:整體介面主區塊。
    • <h1>:標題「Courage's Rooftop AI」。
    • <div id="log">:對話紀錄區域,可能用來顯示與 AI 的互動內容。
    • <input>:文字輸入框,讓使用者輸入問題。帶有以下屬性: id="input":提供 JavaScript 操作用的識別名稱。 placeholder="Type your question here...":提示使用者輸入問題。 autofocus:自動對焦。 autocomplete="off":關閉自動填寫。 spellcheck="false":關閉拼字檢查。

<script src="script.js"></script>

  • 引入 JavaScript 程式檔案,預期會控制使用者輸入與回應邏輯


再來介紹JavaScript 程式檔案

const input = document.getElementById("input");
const log = document.getElementById("log");

// 動態輸出文字(打字機效果)
function typeWriter(text, callback) {
let i = 0;
function typing() {
if (i < text.length) {
log.innerText += text.charAt(i);
i++;
log.scrollTop = log.scrollHeight;
setTimeout(typing, 30);
} else {
log.innerText += "\n\n";
log.scrollTop = log.scrollHeight;
if (callback) callback();
}
}
typing();
}

// 更新閃爍游標位置
input.addEventListener('input', () => {
const pos = input.selectionStart;
input.style.setProperty('--caret-pos', pos);
});
input.addEventListener('keydown', () => {
const pos = input.selectionStart;
input.style.setProperty('--caret-pos', pos);
});

input.addEventListener("keydown", async function (e) {
if (e.key === "Enter") {
const message = input.value.trim();
if (!message) return;
log.innerText += `> ${message}\n`;
input.value = "";
input.style.setProperty('--caret-pos', 0);

try {
const response = await fetch("https://courage-the-cowardly-dog-computer-backend.onrender.com/ask", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
});

const data = await response.json();
if (data.reply) {
// 如果有音訊,先播放
if (data.audio) {
const audioSrc = `data:audio/mp3;base64,${data.audio}`;
const audio = new Audio(audioSrc);
audio.play();
}
// 同步打字機效果
typeWriter(`🖥️ ${data.reply}`);
} else {
log.innerText += "🖥️ Error: " + data.error + "\n\n";
}
} catch (err) {
log.innerText += "🖥️ Network error.\n\n";
}
}
});

1. DOM 元素取得

const input = document.getElementById("input");
const log = document.getElementById("log");
  • input:對應畫面中的文字輸入框,讓使用者輸入指令或問題。
  • log:對應對話記錄區域,用來串接並顯示過去的輸入與 AI 回應。

2. 打字機效果 - typeWriter

function typeWriter(text, callback) { ... }
  • 逐字將 text 內容輸出到 log,每 30 ms 打一個字,營造打字機視覺效果。
  • 打完後自動換兩行 (\n\n) 並捲到最底 (scrollTop = scrollHeight)。
  • 支援 callback,方便在打字完成後接續其他動作。

3. 追蹤游標位置(可搭配 CSS 自訂 caret 動畫)

input.addEventListener('input', () => { ... });
input.addEventListener('keydown', () => { ... });
  • 監聽使用者打字或按鍵,讀取 input.selectionStart(文字插入點位置)。
  • 將值寫入 CSS 變數 --caret-pos,可用於自訂閃爍游標的動畫或定位。

4. 送出訊息與取得回覆

input.addEventListener("keydown", async function (e) { ... });
  • Enter 鍵觸發:
    1. 取出並清空使用者輸入 (message)。
    2. 在 log 中先寫入 > 使用者內容 作為指令歷史。
    3. 使用 fetch 向後端 /ask API POST JSON:{ message }。
  • 等待回應
    • 若成功 (data.reply) 若後端有附 音檔 (Base64 MP3) → 立即轉成 Audio 物件播放。 之後用 typeWriter 將 AI 回覆逐字輸出到 log(帶電腦 emoji🖥️)。
    • 若失敗 (data.error) → 顯示錯誤訊息。
    • 網路錯誤則顯示 "Network error."。

css 就是一些瑣碎的樣式設定就不說了

後端目錄架構設置

raw-image

app.py

1. 基本設定與相依模組

from flask import Flask, request, jsonify
from flask_cors import CORS
from computer_logic import query
import os, base64, json
from google.cloud import texttospeech
from google.oauth2 import service_account
  • Flask / CORS:建立 Web API,並允許跨網域請求(前端在其他網域也能存取)。
  • computer_logic.query:你自訂的函式,用來向 AI (如 ChatGPT/Gemini/RAG) 取得回覆。
  • Google Cloud Text-to-Speech:把 AI 回覆轉成語音。
  • os / json:讀取環境變數並解析服務帳戶金鑰。

2. 建立應用與跨域支援

app = Flask(__name__)
CORS(app)
  • 啟動 Flask 應用實例並開啟 CORS,方便前端呼叫 /ask API。

3. Google TTS 驗證與客戶端

credentials = service_account.Credentials.from_service_account_info(
json.loads(os.environ.get("GOOGLE_APPLICATION_CREDENTIALS_JSON"))
)
tts_client = texttospeech.TextToSpeechClient(credentials=credentials)
  • 金鑰來源:你把 Google Cloud 服務帳戶的 JSON 內容放在環境變數 GOOGLE_APPLICATION_CREDENTIALS_JSON
  • tts_client:透過憑證建立 Cloud Text-to-Speech 客戶端。

4. 文字 ➜ 語音工具函式

def text_to_speech(text):
synthesis_input = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code="cmn-CN",
name="cmn-CN-Chirp3-HD-Fenrir"
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3
)
response = tts_client.synthesize_speech(
input=synthesis_input, voice=voice, audio_config=audio_config
)
return base64.b64encode(response.audio_content).decode("utf-8")
  • 語音模型:指定中文普通話語音 cmn-CN-Chirp3-HD-Fenrir
  • 輸出格式:MP3。
  • 回傳值:把二進位音訊內容轉成 Base64 字串,方便用 JSON 傳給前端。

5. 核心 API - /ask

@app.route("/ask", methods=["POST"])
def ask():
data = request.get_json()
user_message = data.get("message", "")
if not user_message:
return jsonify({"error": "No message provided"}), 400
try:
reply = query(user_message) # 向 AI 取得回覆
audio_base64 = text_to_speech(reply) # 同步轉語音
return jsonify({"reply": reply, "audio": audio_base64})
except Exception as e:
return jsonify({"error": str(e)}), 500
  1. 接收 JSON:前端 POST 來的 {"message": "...使用者輸入..."}
  2. 檢查空訊息:若沒內容回 400。
  3. 呼叫 AI:透過 query 取得文字回覆。
  4. 文字轉語音:呼叫 text_to_speech,回傳 Base64 音訊。
  5. 成功回應{"reply": "...", "audio": "...base64..."}
  6. 錯誤處理:任何例外回 500,並附上錯誤訊息。

6. 啟動伺服器

if __name__ == "__main__":
app.run(debug=True)
  • 直接執行檔案時,以 debug 模式(自動重載、詳細追蹤)在本機啟動。
  • 部署到 Render/GCP 時可改用 gunicornuvicorn,並將 debug=False

computer_logic.query

長期記憶 + RAG + Gemini API 後端模組

1. 主要載入的套件

類別套件用途向量檢索langchain.vectorstores.FAISS、HuggingFaceEmbeddings目前尚未在程式裡實際使用(可能準備後續改成向量索引)。

NLP/特徵sklearn 的 TfidfVectorizer、cosine_similarity將歷史對話轉成 TF-IDF 向量並計算餘弦相似度,做簡易語意檢索。

資料庫psycopg2連接 PostgreSQL,讀寫 conversation_history 表。

環境變數dotenv讀 .env 檔,取得 GEMINI_API_KEY、DATABASE_URL 等。

HTTPrequests向 Gemini REST API 發送 JSON 請求。

2. Gemini API 基本設定

API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=..."
HEADERS = {"Content-Type": "application/json"}
  • 把 API Key 嵌在 URL 參數。
  • 一律以 application/json 傳遞 payload

3. PostgreSQL 永久對話歷史

3.1 讀取 (load_history)

  • role(預設 user)與 n 筆數,抓出最新 n 筆並反向回傳(變成由舊到新)。
  • 格式化成 Gemini 需要的 {"role": "...", "parts": [{"text": "..."}]}

3.2 寫入 (save_history)

  1. INSERT:把 history 裡的新訊息批次寫到 conversation_history
  2. 容量檢查:計算整張表佔用空間 pg_total_relation_size
  3. 自動清理:若超過 size_limit(預設 0.9 GB),就迴圈 DELETE 最舊一筆直到落回上限。

這段機制即是你之前想要的「在 Render 256 MB 免費額度內自動修剪」。


4. RAG—檢索相關訊息

def retrieve_similar(history, query_text, k=3):
tfidf_matrix = vectorizer.fit_transform(texts) # 歷史訊息向量化
query_vec = vectorizer.transform([query_text])
similarities = cosine_similarity(query_vec, tfidf_matrix).flatten()
top_indices = similarities.argsort()[-k:][::-1] # 取前三高
  • TF-IDF + Cosine:雖不是語意向量,但輕量、部署簡單、0 依賴 GPU。
  • 回傳前 k 則最相似的歷史片段,作為「長期記憶」。

之後若想提升品質,可把「FAISS + Embeddings」改為真正的語意檢索。


5. query() 核心流程

  1. 載入長期記憶
    full_history = load_history(role='user', n=100)
    只抓使用者歷史,節省 token。
  2. 抽取相關長期記憶 (retrieve_similar)
  3. 短期記憶
    short_window = full_history[-10:]  # 最近 10
  4. 組合成 Gemini contents
    • 依序塞入: 說明「以下是長期記憶」 3 則相關歷史 「以下是短期記憶」 最近 10 則 「現在使用者提問」 當前 user_message
  5. 系統指令
    你是一個自大且嘲諷的AI,住在屋頂上...
    告訴模型保持毒舌、機智又實用的回答風格。
  6. 呼叫 Gemini
    resp = requests.post(API_URL, headers=HEADERS, json=payload)
    ...
    ai_response = candidates[0]["content"]["parts"][0]["text"]
  7. 持久化
    將本輪 user / model 兩筆對話 append 到 full_history,再呼叫 save_history 寫回 DB,並自動清理空間。
  8. 回傳文字給 Flask 路由
    Flask 會進一步把它轉成語音並回給前端。

資料庫

raw-image
raw-image

一個簡單的SQL資料庫 有 ID ROLE CONTENT CREATED_AT欄位

RENDER也提供

raw-image

可以從外部寫程式讀取資料表

raw-image

最後為了要還原劇中效果我主要在computer_logic設定過

full_history = load_history(role='user',n=100) 

# RAG:從full_history找出與 user_message 最相近的 k 則訊息
retrieved = retrieve_similar(full_history, user_message, k=3)

# 固定保留最近10則對話作短期記憶
short_window = full_history[-10:]

# 組合成「檢索到的長期記憶」+「短期記憶」+「新 user 提問」
context_for_llm = [
{
"role": "system",
"parts": [{"text": "以下是從長期記憶中檢索到的相關訊息:"}]
}
] + retrieved + [
{
"role": "system",
"parts": [{"text": "以下是近期對話記憶(短期記憶):"}]
}
] + short_window + [
{
"role": "system",
"parts": [{"text": "現在使用者提出了新的問題:"}]
},
{
"role": "user",
"parts": [{"text": user_message}]
}
]

讓他可以用RAG從歷史數據100則中提取幾則相關的 以及固定保留最近10則對話作短期記憶 然後跟新問題 一併考慮進去 避免說話沒有記憶的問題

也方便我後續跟他聊天的時候直接把膽小狗英雄的世界觀輸入讓他記錄進資料庫

不用在寫程式改

然後每次要他回覆的時候都設置system_instructio

"system_instruction": {

"parts": [{

"text": (

"你是一個自大且嘲諷的AI,住在屋頂上,風格類似《膽小狗英雄》裡的毒舌電腦。"

"請用機智嘲諷但有用的方式回答問題。"

"你會收到三種訊息:檢索到的長期記憶、短期記憶、以及最新提問,請好好利用這些資訊。"

)

}]

}

這樣就會盡量還原了

raw-image


留言
avatar-img
留言分享你的想法!
avatar-img
于正龍(Ricky)的沙龍
46會員
71內容數
人工智能工作經驗跟研究
2025/03/05
你做錯了。你剛剛發給 ChatGPT 的「寫一個函式來……」的提示?刪掉它吧。這些通用提示就是為什麼你的編碼速度還跟其他人一樣的原因。 在與 AI 進行超過 3,000 小時的結對編程後,我發現了真正有效的方法——而這並不是你想的那樣。 真相是:85% 的開發者陷入了 AI 驅動的複製粘貼循環。
2025/03/05
你做錯了。你剛剛發給 ChatGPT 的「寫一個函式來……」的提示?刪掉它吧。這些通用提示就是為什麼你的編碼速度還跟其他人一樣的原因。 在與 AI 進行超過 3,000 小時的結對編程後,我發現了真正有效的方法——而這並不是你想的那樣。 真相是:85% 的開發者陷入了 AI 驅動的複製粘貼循環。
2025/03/05
簡介 — 我如何停止浪費時間的故事 幾年前,我意識到我花在“做事”上的時間比實際在專案上取得進展的時間要多。我醒來時有無休止的待辦事項清單、回復電子郵件、參加會議、審查檔,但到一天結束時,我覺得我實際上沒有在任何重要的事情上取得進展。 有一天,一個朋友告訴我: 忙碌並不等同於有效。 這讓
2025/03/05
簡介 — 我如何停止浪費時間的故事 幾年前,我意識到我花在“做事”上的時間比實際在專案上取得進展的時間要多。我醒來時有無休止的待辦事項清單、回復電子郵件、參加會議、審查檔,但到一天結束時,我覺得我實際上沒有在任何重要的事情上取得進展。 有一天,一個朋友告訴我: 忙碌並不等同於有效。 這讓
2023/09/30
看到滿多年輕工程師提問:工作時經常查 ChatGPT,感覺不太踏實,沒關係嗎? 讓我簡單談論一下這件事 --- 首先,讓我們把時間倒回 2000 年代 google 剛出來的時候 當時一定也是這樣, 年輕工程師遇到問題狂查 google 資深工程師則覺得 google 可有可無,
2023/09/30
看到滿多年輕工程師提問:工作時經常查 ChatGPT,感覺不太踏實,沒關係嗎? 讓我簡單談論一下這件事 --- 首先,讓我們把時間倒回 2000 年代 google 剛出來的時候 當時一定也是這樣, 年輕工程師遇到問題狂查 google 資深工程師則覺得 google 可有可無,
看更多
你可能也想看
Thumbnail
我從右腦 走到左腦 憑藉的是一把小手槍 它的子彈有點俏皮 具有清洗的功能
Thumbnail
我從右腦 走到左腦 憑藉的是一把小手槍 它的子彈有點俏皮 具有清洗的功能
Thumbnail
照夜白電腦繪圖作品。 使用軟體:Photoshop CSP。 雖然玩法爭議很多, 在這速食的時代遊戲流程略顯慢調, 但不得不說美術真的美麥, 人物設計都有針到穴位點上, 3D、動作、特效都很漂亮, 先畫一隻黑肉小貓。
Thumbnail
照夜白電腦繪圖作品。 使用軟體:Photoshop CSP。 雖然玩法爭議很多, 在這速食的時代遊戲流程略顯慢調, 但不得不說美術真的美麥, 人物設計都有針到穴位點上, 3D、動作、特效都很漂亮, 先畫一隻黑肉小貓。
Thumbnail
用人工智慧編輯過世的狗狗 #微軟設計師
Thumbnail
用人工智慧編輯過世的狗狗 #微軟設計師
Thumbnail
上次介紹了老鼠戰隊,既然有戰隊後方一定有一名科學家來研發道具支援 牠就是-老鼠吱吱博士
Thumbnail
上次介紹了老鼠戰隊,既然有戰隊後方一定有一名科學家來研發道具支援 牠就是-老鼠吱吱博士
Thumbnail
[大聲點] 什麼? 叫我不要說的太小聲 說大聲一些、清楚一點 好!! 我很棒 我真的很棒 我超級棒的啦
Thumbnail
[大聲點] 什麼? 叫我不要說的太小聲 說大聲一些、清楚一點 好!! 我很棒 我真的很棒 我超級棒的啦
Thumbnail
長期壓抑的傷痛,至此爆發… (下期開始,改為不定期發佈,感謝至今為止的支持,至少怪人篇會畫完)
Thumbnail
長期壓抑的傷痛,至此爆發… (下期開始,改為不定期發佈,感謝至今為止的支持,至少怪人篇會畫完)
Thumbnail
DALL.E魔鏡 魔鏡!魔鏡! 幫我生成Kitty貓、LINE熊大,並且要他們大亂鬥! DALL.E 魔鏡:好的,Kitty Cat與Line熊大的模樣生成中... DALL.E 魔鏡:凱蒂貓戰士生成完畢。 DALL.E 魔鏡:對手惡霸貓生成完畢。 遊戲開始 凱蒂貓 V.S 惡霸貓
Thumbnail
DALL.E魔鏡 魔鏡!魔鏡! 幫我生成Kitty貓、LINE熊大,並且要他們大亂鬥! DALL.E 魔鏡:好的,Kitty Cat與Line熊大的模樣生成中... DALL.E 魔鏡:凱蒂貓戰士生成完畢。 DALL.E 魔鏡:對手惡霸貓生成完畢。 遊戲開始 凱蒂貓 V.S 惡霸貓
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News