

這部很老的卡通了 一直對膽小狗英雄頂樓那台電腦很有印象
毒舌 嘴砲 自大 嘲諷 但總是能給膽小狗有用的建議 幫助他解決各種問題
最近剛好比較多時間在寫程式 又看到現在LLM技術發展成熟基本上可以實現跟卡通裡面一模一樣的效果了
整理一下功能
1.毒舌 嘴砲 自大 嘲諷 但總是能給膽小狗有用的建議 幫助他解決各種問題
2.有語音功能
3.有記憶功能
4.有一個簡單的介面
整理好了就開始規劃
我設計成3層式架構 前端 後端 資料庫 分離開來比較好維護 管理 跟擴充

表現層就是UI介面設計我用html css javascript設計
業務邏輯層就是程式的運算跟邏輯 我使用 python flask
資料存取層就是放資料我用Postgres SQL儲存資料
部屬方式就是放在雲端RENDER上

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| | (第三方服務) |
+--------+ +----------------+
| ^
| |
+--------------+
回傳資料
前端資料夾目錄架構

前端介面就這樣很簡單但好用
<!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>

📄 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 鍵觸發:
- 取出並清空使用者輸入 (message)。
- 在 log 中先寫入 > 使用者內容 作為指令歷史。
- 使用 fetch 向後端 /ask API POST JSON:{ message }。
- 等待回應:
- 若成功 (data.reply) 若後端有附 音檔 (Base64 MP3) → 立即轉成 Audio 物件播放。 之後用 typeWriter 將 AI 回覆逐字輸出到 log(帶電腦 emoji🖥️)。
- 若失敗 (data.error) → 顯示錯誤訊息。
- 網路錯誤則顯示 "Network error."。
css 就是一些瑣碎的樣式設定就不說了
後端目錄架構設置

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
- 接收 JSON:前端 POST 來的
{"message": "...使用者輸入..."}
。 - 檢查空訊息:若沒內容回 400。
- 呼叫 AI:透過
query
取得文字回覆。 - 文字轉語音:呼叫
text_to_speech
,回傳 Base64 音訊。 - 成功回應:
{"reply": "...", "audio": "...base64..."}
。 - 錯誤處理:任何例外回 500,並附上錯誤訊息。
6. 啟動伺服器
if __name__ == "__main__":
app.run(debug=True)
- 直接執行檔案時,以 debug 模式(自動重載、詳細追蹤)在本機啟動。
- 部署到 Render/GCP 時可改用
gunicorn
或uvicorn
,並將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
)
- INSERT:把
history
裡的新訊息批次寫到conversation_history
。 - 容量檢查:計算整張表佔用空間
pg_total_relation_size
。 - 自動清理:若超過
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()
核心流程
- 載入長期記憶
full_history = load_history(role='user', n=100)
只抓使用者歷史,節省 token。 - 抽取相關長期記憶 (
retrieve_similar
) - 短期記憶
short_window = full_history[-10:] # 最近 10 則
- 組合成 Gemini contents
- 依序塞入: 說明「以下是長期記憶」 3 則相關歷史 「以下是短期記憶」 最近 10 則 「現在使用者提問」 當前 user_message
- 系統指令
你是一個自大且嘲諷的AI,住在屋頂上...
告訴模型保持毒舌、機智又實用的回答風格。 - 呼叫 Gemini
resp = requests.post(API_URL, headers=HEADERS, json=payload)
...
ai_response = candidates[0]["content"]["parts"][0]["text"] - 持久化
將本輪user / model
兩筆對話 append 到full_history
,再呼叫save_history
寫回 DB,並自動清理空間。 - 回傳文字給 Flask 路由
Flask 會進一步把它轉成語音並回給前端。
資料庫


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

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

最後為了要還原劇中效果我主要在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,住在屋頂上,風格類似《膽小狗英雄》裡的毒舌電腦。"
"請用機智嘲諷但有用的方式回答問題。"
"你會收到三種訊息:檢索到的長期記憶、短期記憶、以及最新提問,請好好利用這些資訊。"
)
}]
}
這樣就會盡量還原了
