🆕 新增/修改的程式碼
封裝成 speak(),可被 CLI 與 Next.js API 重用。
// src/day9_text_to_speech.js
import fs from "fs";
import path from "path";
import { openai } from "./aiClient.js";
/**
if (!text || !text.trim()) throw new Error("文字內容 text 為必填。");
if (speed < 0.5 || speed > 1.5) throw new Error("speed 建議介於 0.5 ~ 1.5 之間。");
if (!["mp3", "wav", "opus"].includes(format)) throw new Error("format 僅支援 mp3|wav|opus。");
// 使用 OpenAI Audio Speech API
// 註:官方 SDK 常見用法為 openai.audio.speech.create({ model, voice, input, format, speed })
const res = await openai.audio.speech.create({
model,
voice,
input: text,
format, // 有些版本為 "audio_format",此處以 SDK 當前主參數為準
speed,
});
const arrayBuffer = await res.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
const base = filename
? filename.replace(/.[^.]+$/, "")
: tts_${Date.now()}
;
const filepath = path.join(outputDir, ${base}.${format}
);
fs.writeFileSync(filepath, buffer);
return { filepath, bytes: buffer.length };
}
/** 便利方法:讀取檔案並唸出(例如把 Day 8 的逐字稿 txt 直接轉語音) */
export async function speakFromFile(filePath, opts = {}) {
if (!fs.existsSync(filePath)) throw new Error(找不到檔案:${filePath}
);
const text = fs.readFileSync(filePath, "utf-8");
return speak({ ...opts, text });
}
const args = Object.fromEntries(
process.argv.slice(2).reduce((acc, cur, i, arr) => {
if (cur.startsWith("--")) {
const key = cur.replace(/^--/, "");
const val = arr[i + 1] && !arr[i + 1].startsWith("--") ? arr[i + 1] : true;
acc.push([key, val]);
}
return acc;
}, [])
);
async function main() {
const task = args.task || "chat";
if (task === "tts") {
const text = args.text || "";
const file = args.file || ""; // 若提供文字檔(.txt),直接轉語音
const model = args.model || process.env.OPENAI_TTS_MODEL || "gpt-4o-mini-tts";
const voice = args.voice || "alloy";
const format = args.format || "mp3";
const speed = args.speed ? Number(args.speed) : 1.0;
const filename = args.out || undefined;
if (file) {
const { filepath, bytes } = await speakFromFile(file, { model, voice, format, speed, filename });
console.log("\n=== 文字檔 → 語音 ===");
console.log("輸出:", filepath, `(${bytes} bytes)`);
} else {
const content = text || "這是一段測試用的語音。";
const { filepath, bytes } = await speak({ text: content, model, voice, format, speed, filename });
console.log("\n=== 文字 → 語音 ===");
console.log("輸出:", filepath, `(${bytes} bytes)`);
}
} else if (task === "stt") {
const filePath = args.filePath || null;
const url = args.url || null;
const language = args.lang || "";
const prompt = args.prompt || "";
const detailed = args.detailed === "true" || args.detailed === true;
const { text, saved } = await transcribe({ filePath, url, language, prompt, detailed });
console.log("\n=== 語音轉文字(STT) ===\n");
console.log(text);
console.log("\n已儲存:", saved);
} else if (task === "vision") {
const imagePath = args.imagePath || null;
const imageUrl = args.imageUrl || null;
const wantOCR = args.ocr === "true" || args.ocr === true;
const length = args.length || "medium";
const out = await imageToJson({ imagePath, imageUrl, wantOCR, length });
console.log("\n=== 圖片 → JSON 描述 ===\n");
console.log(JSON.stringify(out, null, 2));
} else if (task === "image") {
const prompt = args.text || "一隻戴著太空頭盔的柴犬,漂浮在月球上,插著台灣國旗";
const size = args.size || "512x512";
const n = args.n ? Number(args.n) : 1;
const urls = await textToImage(prompt, { size, n });
console.log("\n=== 生成圖片 ===\n");
urls.forEach((f) => console.log("已儲存:" + f));
} else if (task === "chat") {
const sessionId = args.session || "default";
if (args.reset) {
resetSession(sessionId);
console.log(已重設會話:${sessionId}
);
return;
}
const input = args.text || "嗨,我想規劃 3 天 2 夜的台中旅遊行程。";
const { reply } = await chatOnce(input, { sessionId });
console.log(\n[${sessionId}] AI:\n${reply}\n
);
} else if (task === "teacher") {
const out = await englishTeacher(args.text || "He go to school every day.");
console.log("\n=== 英文老師 ===\n");
console.log(out);
} else if (task === "review") {
const out = await codeReview("function sum(arr){ return arr.reduce((a,b)=>a+b,0) }");
console.log("\n=== 程式碼審查 ===\n");
console.log(out);
} else if (task === "sentiment") {
const out = await sentimentClassify(args.text || "今天心情糟透了,事情一團亂。");
console.log("\n=== 情緒分類(JSON) ===\n");
console.log(out);
} else if (task === "json_summary") {
const out = await newsToJson(args.text || "OpenAI 發布新模型,效能大幅提升。");
console.log("\n=== 新聞 JSON 摘要 ===\n");
console.log(out);
} else {
console.log("未知任務,請使用 --task tts | stt | vision | image | chat | teacher | review | sentiment | json_summary");
}
}
main().catch((e) => {
console.error("發生錯誤:", e.message);
process.exit(1);
});
你可以用 --model tts-1 測試不同模型(若你的帳戶有開啟)。
也能用 --out my_demo 自訂檔名:會輸出 outputs/tts/my_demo.mp3。
▶️ CLI 測試
npm run day9:tts --silent
npm run day9:tts:file --silent
node index.js --task tts --text "早安,今天開始第九天的挑戰。" --format wav --speed 0.9
輸出範例:
outputs/tts/tts_17264xxxxx.mp3