ð¥åç»çæAIçšã®ãããªãã¬ã€ã€ãŒã Electronã§ äœã£ãããGPUãæšãŠã矜ç®ã«ãªãããã£ã話
0.ã¯ããã«
ããŒã«ã«ã«æºã蟌ãã åç»ãã¡ã€ã«ãã©ããã£ãŠç®¡çããŠããŸããïŒ
å人çã«ãPinterest颚ã®ãMasonryã¬ã€ã¢ãŠããã§äžèŠ§è¡šç€ºããŠãã¹ã¯ããŒã«ããã ãèªååçãããããŒã«ã«ã¢ããªã欲ãããã§ããã...ãå¯èœã§ããã°ã¢ãã€ã«å¯Ÿå¿ããããã€ã
ãã¡ããMasonryã¬ã€ã¢ãŠããªæçš¿ãµã€ãã䜿ãã°äŒŒããããªããšã¯ã§ããŸããããã¡ãã¡ã¢ããããŒãããã®ãé¢åã ãã人ã«èŠããããªããã®ããããããŒã«ã«ã§ã管ççšã®ãã®ãªããªãã¯ãªãããã¡ãã£ãšèªåã®æ±ããŠãããã®ãšã¯æ¯è²ãéãã
åç»ã¢ããªéçºãã¢ãã€ã«å¯Ÿå¿ã®ç¥èãªããŠãŒãã§ãããä»ã©ãAIãããã°ãªããšããªãã£ãããšããããšã§ããã£ã¡ãäœã£ãŠã¿ãŸãããElectron ã«ãã¡ãã£ãšè§Šã£ãŠã¿ããã£ãããšã ãã
ãããªæãã®èŠãç®ã§ããAIæäžžåºãã®ããŒã¯ã¢ãŒãã¬ã€ã¢ãŠãããªããžããªã¯äžçªäžã«ã

ã³ãŒãã®ã»ãŒå
šãŠã AI(ç¡èª²éå¢åŸ¡çšéã®Google AI Studio) ã䜿ãåããŠçæããŠããŸããAIã䜿ã£ãã¢ããªéçºã®äºäŸãšããŠãã䌌ããããªãã®ãäœãããæ¹ã®åèã«ãªãã°å¹žãã§ãã
ä»åã¯ãéçºäžã«ééããElectronã§åç»åçãã¬ã€ã€ãŒãäœãããã®èœãšã穎ã«ã€ããŠå
±æã§ããã°ãšæããŸããäœåçŽ äººãªããã§ééã£ãŠããããšãå€ããšæããŸããééãã¯ææããŠããã ããã°å©ãããŸãã
1.æè¡ã¹ã¿ãã¯ãšæ©èœ
ä»ã®æè¡ã¹ã¿ãã¯ã¯ä»¥äžã®éãã§ããAIãåºããŠããåæã®æ§ææ¡ã«ããZustand + TanStack QueryãšFSDïŒFeature-Sliced DesignïŒäœ¿ã£ãŠããšè¿œå ã§æç€ºããã蟌ã¿ãŸããã
AIã«ã³ãŒããæžããããšèŽ
èãå€ããŠåŸã§ãªãã¡ã¯ã¿ãªã³ã°ã«æéãåãããããšãå€ããã§ããããããå°ãã§ãæžããããã«FSDã©ã€ã¯ãªãã®ãæ¡çšããŠããŸããäŸåæ¹åã®æŽçãå«ããŠã
- Core: Electron, Vite, React, TypeScript
- Frontend Arch: Feature-Sliced Designã©ã€ã¯
- Backend Arch: Service-Repository Pattern
- UI: Tailwind CSS, shadcn/ui, Lucide React
- State: Zustand, TanStack Query
- Database: better-sqlite3 (SQLite)
- Server: Node.js HTTP Server + SSE (Server-Sent Events)
äž»ãªæ©èœã¯ä»¥äžã®éãã§ãããªãã¹ãèµ·åæéãæ©ããªãããã«èšèšããŠããŸãã
- Inode & Partial Hash: ãã¡ã€ã«ã·ã¹ãã èå¥å(Inode)ãšéšåããã·ã¥èšç®ãçµã¿åãããããªããŒã ãç§»åã®è¿œè·¡ã»èªå修埩ããžãã¯ã
- SQLite FTS5: åç»å ã®çæããã³ããïŒJSONïŒãŸã§å¯Ÿè±¡ãšããå šææ€çŽ¢ã
- Metadata Harvester: ãã©ã«ããéããéã«å šãã¡ã€ã«ã®è§£æå®äºãåŸ ããã«äžèЧã衚瀺ããã®åŸããã¯ã°ã©ãŠã³ãã§é 次ã¡ã¿ããŒã¿ãååŸã»åæ ã
- Custom Streaming Server: ExpresséäŸåã®è»œéå®è£ ãšSSEã«ãããªã¢ã«ã¿ã€ã åæã
2.ã«ã¹ã¿ã ãããã³ã«
ä»åã¯ãåç»ãã¬ã€ã€ãŒã®ãåç»ãã¡ã€ã«ã®èªã¿èŸŒã¿ããã¡ã€ã³ã®è©±ã«ãªããŸãã
Electronã§ããŒã«ã«ãã¡ã€ã«ïŒç»åãåç»ïŒã衚瀺ããå Žåãã»ãã¥ãªãã£ã®èгç¹ãã webSecurity: falseã®èšå®ãå¿
èŠãªfile:// ãããã³ã«ãçŽæ¥äœ¿ãã®ã¯éæšå¥šãšãããŠããŸãã
ãªã®ã§ãå®ç³éãã«ã¹ã¿ã ãããã³ã«ãå®è£
ããŸãããvideo:// ãšãããããã³ã«ãå®çŸ©ããŠããããã¡ã€ã³ããã»ã¹ã§ããã¯ããŠãã¡ã€ã«ãè¿ãã
// ã¡ã€ã³ããã»ã¹ (倱æäŸ)
protocol.handle('video', async (request) => {
const filePath = request.url.replace('video://', '');
// ...ãã¹ã®ãã³ãŒãåŠç...
return net.fetch(`file:///${filePath}`);
});
ããã鿢ç»ãªãå šãåé¡ãªããã§ãããåç»ãã¬ã€ã€ãŒãšããŠæ¬æ Œçã«äœ¿ãããšãããšãRange RequestïŒéšåèªã¿èŸŒã¿ïŒ ã®åŠçãå£ã«ãªããŸããã
è¡åã®æªãAIçæåç»
ãã©ãŠã¶ã®åç»ãã¬ã€ã€ãŒã¯ãåç»ãå¹çããåçããããã«ããããããããŸã§ïŒäŸïŒ0ãã€ãã1MBïŒããšããéšåçãªãªã¯ãšã¹ããæããŸãã
Electronã® net.fetch ã protocol.handle ã§ãããå®ç§ã«ãã°ãã«ã¯ããªã¯ãšã¹ãããããŒã® Range ãè§£æããŠãã¬ã¹ãã³ã¹ããããŒã«æ£ãã Content-Range ãä»ããŠâŠâŠãšããåŠçãèªåã§å®è£
ããå¿
èŠããããŸãã
ãããäžæããã£ãŠããªããšããã©ãŠã¶åŽããåç»ã®ã¡ã¿ããŒã¿ãæ£ããèªã¿åããªãããã·ãŒã¯ãªã©ã®æåãäžå®å®ã«ãªãããšãã£ãåé¡ãåŒãèµ·ãããŸããããã«èŽåœçã ã£ãã®ããAIçæåç»ç¹æã®è¡åã®æªãåç»ã§ãµã ãã€ã«ïŒåæãã¬ãŒã ïŒã衚瀺ããããã°ã¬ãŒã®åè§ãç®±ã衚瀺ãããã ãã«ãªãçŸè±¡ã§ããããã®åç»ã«ã€ããŠã¯åŸè¿°ããŸãã
åçãã¿ã³ãæŒãã°æ ããã§ãããäžèЧæ§ãèããäœãããæ£ããå®è£ ããã°åãããšã¯ãããåç»é ä¿¡ãµãŒããŒã®ããžãã¯ããããã³ã«ãã³ãã©å ã§åçºæããã®ã¯éªšãæããŸãã
3.ã¢ããªå ã§Node.jsã®HTTPãµãŒããŒã飌ã
ããã©ãŠã¶ïŒChromiumïŒãäžçªåŸæãªã®ã¯äœãïŒããšèãããããã£ã±ãæ®éã® HTTPéä¿¡ ã
å€ã«Electronã®ãããã³ã«ãã³ãã©ã§èªåå®è£
ãé 匵ããããã¢ããªå
ã§ localhost ã®HTTPãµãŒããŒãç«ãŠãŠãããããé
ä¿¡ããã»ããç¢ºå® ãšAIããææ¡ããããŸããã
ãšããããã§ãã¡ã€ã³ããã»ã¹èµ·åæã« http ã¢ãžã¥ãŒã«ã§ãµãŒããŒãç«ã¡äžããŸããã
// electron/lib/local-server.ts (æç²)
import http from 'http';
import fs from 'fs';
// å®éã®å®è£
ã§ã¯media-handler.tsã«ããžãã¯ãåé¢ããŠããŸãããæŠå¿µçã«ã¯ãããªæãã§ã
const startLocalServer = () => {
const server = http.createServer((req, res) => {
// ... (ãã¹ååŸãã»ãã¥ãªãã£ãã§ãã¯) ...
const stat = fs.statSync(filePath);
const fileSize = stat.size;
const range = req.headers.range;
// Range Requestã®åŠç
if (range) {
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunksize = end - start + 1;
const file = fs.createReadStream(filePath, { start, end });
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4', // å®éã¯mime-typesçã§å€å¥
});
file.pipe(res);
} else {
// ...éåžžã¬ã¹ãã³ã¹...
res.writeHead(200, {
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
});
fs.createReadStream(filePath).pipe(res);
}
});
// ããŒã0ãæå®ããŠç©ºããŠããããŒããèªåå²ãåœãŠ
server.listen(0, '127.0.0.1');
};
ããã§ HTTP çµç±ã§åç»ãé ä¿¡ã§ããããã«ãªãã
- ãµã ãã€ã«è¡šç€ºïŒã°ã¬ãŒã ã£ãåç»ãããããªè¡šç€º
ããã§è§£æ±ºïŒ...ãšæããããããã€ãåçã®éäžã§ã¹ã¿ãã¯ããŸããããããã»ããšãªããªãã ããã€ãã
çµå±ãHTTPãµãŒããŒãç«ãŠãŠãæ ¹æ¬çãªè§£æ±ºã«ã¯ãªããŸããã§ããã
4.ç¡é§ãªããã
VLCãã¬ã€ã€ãŒã§ã¯åé¡ãªãåçã§ããã®ã«ããã®ã¢ããªïŒChromiumïŒã§ã ãæ¢ãŸã£ãŠããŸãã
VLCãã¬ã€ã€ãŒã®è©²åœåç»ã®ãã°ãAIã«èªãŸããŠã¿ããšãã©ãããã¿ã€ã ã¹ã¿ã³ãããããåç»ã§èµ·ããŠããŸããã
åé¡ã®åç»ã¯ãæ åããŒã¿ã 0.03ç§ å°ç¹ããå§ãŸã£ãŠããã
ãã©ãŠã¶ã® <video> ã¿ã°ã¯ãåŸåã«ããã©ã«ãã§ 0.0ç§ ããåçããããšããŸããããããããã«ã¯ããŒã¿ããªãïŒèç¡ïŒããããã³ãŒããŒãæ··ä¹±ããŠã¹ã¿ãã¯ããŠããŸãâŠâŠãšããç¶æ
ã®ããã§ãïŒAIãèšãã«ã¯ïŒã
ãããããªãã®ã§ããããã詊ããŠã¿ãŸããã
- ã¡ã¢ãªãªãŒã¯ãé²ãããã«
<video loop>屿§ã䜿ãããonEndedã€ãã³ãã§currentTime = 0ã«æ»ããŠplay()ãããå¹æäžæã - 0ç§ã§ã¯ãªãã0.1ç§ããåçããããã«ããŠã¿ãã广ãªãããã³ãŒããŒã®éšåã§ã¹ã¿ãã¯ããŠããããæç»ã®éšåã ãããã£ãŠãæå³ããªããïŒãã ããè² è·åæ£ãšããŠã¹ã¯ããŒã«æã®ã¬ã¿ã€ã軜æžã«å¹æããã
ãè©°ãã ïŒããšGeminiã«èããšããGPUã¢ã¯ã»ã©ã¬ãŒã·ã§ã³åããŸãããã ãšâŠâŠããžïŒ
ãšããããšã§ãGPUãåã£ãŠCPUã«ããããšã§ã¹ã¿ãã¯åé¡ãç¡äºè§£æ±ºããŸããã
ãã£ãã(ã¿ã€ãã«åå)ïŒ
ããåœããåã§ããGPUãã³ãŒããç¡å¹åããããšã§ãå¯äœçšãšã㊠åç»ã®èªã¿èŸŒã¿ãç®ã«èŠããŠé
ããªããŸããã
ã¹ã¯ããŒã«ãããã³ã«ãåç»ã衚瀺ããããŸã§æ°ç§éãã°ã¬ãŒã®åè§ãã衚瀺ãããã®ã¯ãæ£çŽã¡ãã£ãšäœéšãæªãã§ããGPU䜿ããã...ã
ãããããªãã®ã§ã2ç§ã¹ã¿ãã¯ããã匷å¶åãªããŒãããããšã§èª€éåãããšã«ããŸããã
// src/entities/video/model/use-video-playback.ts (æç²)
// åçãæ¢ãŸã£ããã®ç£èŠããžãã¯ïŒWatchdogïŒ
useEffect(() => {
// ãã€ãã£ãåçã§ããªã圢åŒ(ffmpeg倿ç)ã¯å¯Ÿè±¡å€
if (!canPlayNative) return;
let watchdogInterval: NodeJS.Timeout;
// ç»é¢å
ã«ããŠ(inView)ããã€åç»ãããŒããã¹ãç¶æ
ãªã®ã«...
if (inView && shouldLoadVideo) {
// 2ç§ããšã«ãã§ãã¯ãç¶ãã
watchdogInterval = setInterval(() => {
const vid = videoRef.current;
// readyState < 3 (HAVE_FUTURE_DATA: åçå¯èœãªããŒã¿ããªã) ã®ãŸãŸã¹ã¿ãã¯ããŠããã
if (inView && vid && vid.readyState < 3) {
console.warn(`[Watchdog] Stuck detected. Retrying... ${video.name}`);
// StateãæŽæ°ããŠåŒ·å¶çã«ã³ã³ããŒãã³ããåã¬ã³ããªã³ã°ããã
// URLã®ã¯ãšãªãã©ã¡ãŒã¿ã« `_v=${viewId}` ãä»äžããŠããã®ã§ã
// ãããã€ã³ã¯ãªã¡ã³ãããããšãã©ãŠã¶ã¯ãæ°ããåç»ã ããšèªèããŠãã³ãŒããŒããªã»ãããã
setViewId((prev) => prev + 1);
}
}, 2000);
}
return () => clearInterval(watchdogInterval);
}, [inView, shouldLoadVideo, viewId, video.name, canPlayNative]);
æ£çŽãæåæã¯ãããã§ããããã§ãããšããŸãããã
å¯å€è§£å床åç»
ããããã¯æšæž¬ã«ãªãã®ã§ã話ååã«èããŠãã ããã
ã§ããããããªãã§ã¿ã€ã ã¹ã¿ã³ãããããŠãã ã£ãŠè©±ã§ãããã¿ã€ã ã¹ã¿ã³ãããããŠããŸã£ãã®ã¯çµæã§ããåå ã§ã¯ãªããšèããããŸããåŸã«ãªã£ãŠãã倿ããŸããããåé¡ã®åç»ã¯éäžã§è§£å床ãå€ãããå¯å€è§£å床åç»ãã ã£ããã§ããããããGPUãèŠæãªå¥æ°è§£å床ã®ã
ãGPUãç©ççã«èŠæãªåŠçïŒå¥æ°è§£å床ïŒããããããããã«éäžã§ãããã¡ãµã€ãºã®å€æŽãŸã§èŠæ±ãããŠãåŠçã远ãã€ããã«ã¹ã¿ãã¯ããçµæãšããŠã¿ã€ã ã¹ã¿ã³ãããããã ...å¯èœæ§ãé«ãã§ãã
äœããïŒ
0ç§å°ç¹ã®è§£å床

1ç§å°ç¹ã®è§£å床

ãã®ãããªåé¡ããããç¡ããªãã§ãããããããã®è§£æ±ºã«æéãæããã®ã¯ç¡é§ãªãã ãããªããšããæ°ãããªãã§ããªãã
5.HTTPãµãŒããŒããã
ããŠãã¹ã¿ãã¯åé¡ã¯èŠåããã€ããã®ã§æ®ãæå€§ã®åé¡ã¯ããã©ãŒãã³ã¹ã
詳ããã¯èª¬æããŸãããã以äžã®ãããªå¯ŸçããšããŸããã
- å¯èŠé åã«å¿ãããªãœãŒã¹å¶åŸ¡ïŒIntersection Observerã«ããDOM管çïŒ
- ã¡ãã£ã¢ã®é å»¶ããŒãïŒé«éã¹ã¯ããŒã«äžã¯èªã¿èŸŒãŸããäžå®æé衚瀺ã§ããŒãïŒ
- ç¡éã¹ã¯ããŒã¹ã®å®è£ ïŒããŒã¿ãå°åãã«ããŠæç»ïŒ
- åã¬ã³ããªã³ã°ã®æå¶ïŒMemoåã«ããæé©åïŒ
- ããã¯ãšã³ãããŒã¿ã®è»œéåïŒäžèŠ§è¡šç€ºã«äžèŠãªå·šå€§ãªã¡ã¿ããŒã¿ãé€å€ããŠååŸïŒ
ä»®æ³ãªã¹ãã¯Masonryã¬ã€ã¢ãŠããšçžæ§ãæªãããªã®ã§ææŠããããŠããŸãããã¡ã¿ããŒã¿ãäºåååŸããŠããã詊ããŠãããã§ãããããããããšèµ·åãé
ããªãã®ã§...ã
ãã®ãããã§ããçšåºŠæ¹åãèŠãããŸããããã©ãããŠãåç»ã®èªã¿èŸŒã¿ãé
ããã©ããåé¡ãªã®ããšAIã«èããŠã¿ããšããã©ãŠã¶ã®åææ¥ç¶æ°å¶éãããã«ããã¯ãããã6åçšåºŠã«å¶éããããšãã
ãšããããšã§6åå¶éãåé¿ããããã«HTTPãµãŒããŒãæšãŠãfile:// ãããã³ã«ã®æ¡çšã決å®ããŸããã
ãããæç¶èªã¿èŸŒã¿ã®ããã©ãŒãã³ã¹éãã
ãšã¯ããfile:// ãããã³ã«ãæ¡çšããããã«ã¯webSecurity: false(2åç®ã§ããéæšå¥šã§ã)ã«ããå¿
èŠããããã»ãã¥ãªãã£çãªæžå¿µãçºçããŸããã¢ãã€ã«é£æºãå
¥ããããšã ãäžå¿ä»¥äžã®ãããªå¯Ÿçãæã£ãŠãããŸããã¢ãã€ã«ããã®ãã¡ã€ã«æäœã念ã®ããåã£ãŠããã穎ã¯ããã®ã§äœ¿çšç°å¢ã«æ³šæãå¿
èŠã
ã»ããã»ã¹ã®ãµã³ãããã¯ã¹å
// electron/main.ts
const mainWindow = new BrowserWindow({
// ...
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // UIãšããªããŒãã®ã³ã³ããã¹ããåé¢
nodeIntegration: false, // UIåŽã§ã®Node.jsæ©èœãç¡å¹å
webSecurity: false, // file:// ã¢ã¯ã»ã¹ã®ããã«ç¡å¹åããã...
},
});
ã» ãã¹ãã©ããŒãµã«å¯Ÿç
// electron/lib/server/security.ts
export const isPathAllowed = (targetPath: string): boolean => {
// ã¢ããªã§èš±å¯ãããŠãããã©ã«ãäžèЧãååŸ
const libraryFolders = store.get('libraryFolders') || [];
const normalizedTarget = path.normalize(targetPath);
// ã¿ãŒã²ãããèš±å¯ãã©ã«ãã®ãé
äžãã«ããããã§ãã¯
return libraryFolders.some((root) => {
const normalizedRoot = path.normalize(root);
return normalizedTarget.startsWith(normalizedRoot);
});
};
ã» CSP (Content Security Policy) ã«ããæ¥ç¶å¶é
<!-- index.html -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' ...; img-src 'self' file: blob: data: http://127.0.0.1:*; connect-src 'self' http://127.0.0.1:* ws://localhost:*"
/>
6.HTTPãµãŒããŒãã£ã±ãã
ã§ãçšæžã¿ã«ãªã£ãHTTPãµãŒããŒã§ããã¢ãã€ã«é ä¿¡çšã«ãµã«ããŒãžããŠäœ¿ããŸãã
éåžžã®Webéçºã§ããã°Express ã Fastify ãšãã£ãWebãã¬ãŒã ã¯ãŒã¯ãæ¡çšãããšå°ãã®åã§ãããElectronã¢ããªã§ã¯èµ·åé床ãžã®åœ±é¿ãé¿ããããªã®ã§ä»åã¯äžæ¡çšãšããŸããä»åã®èŠä»¶ã¯ãåç»ã¹ããªãŒã ããšãSSEãã ããªã®ã§å šãŠAIã«å®è£ ãããŸããã
ãµãŒããŒèªäœã¯å®è£ æžã¿ãªã®ã§ã次ã®åé¡ã¯ããã³ããšã³ãã®PC(IPCéä¿¡)ãš ã¢ãã€ã«(HTTPéä¿¡)ã®æ¡ä»¶åå²ã§ããå šãŠã®ã³ã³ããŒãã³ãã«Â if (isPC) { ... } else { ... }Â ãšæžãã®ã¯é¢åãªã®ã§ Factory Pattern ãå°å ¥ãããã®åå²ãäžç®æã«å°ã蟌ããŸãã
å ·äœçã«ã¯ãElectronã®Â ipcRenderer.invoke 㯠Promise ãè¿ããããæ§é çã«ã¯Â fetch ãªã©ã®HTTPã¯ã©ã€ã¢ã³ããšåçã«æ±ããã®ã§ TanStack QueryÂ ãæŽ»çšãããã®Â queryFnÂ ã«æž¡ãã¯ã©ã€ã¢ã³ããç°å¢ã«å¿ããŠå·®ãæ¿ããŠããŸãã
ãã以äžã¯åç»ãã¬ã€ã€ãŒãšããŸãé¢ä¿ããªãã®ã§å²æããŸãã
7.æåŸã«
å人éçºã¯ããããç¥ããªãããšãå匷ã§ããã®ã§æ¥œããã§ãããã¿ããªããªã¬ãªã¬åç»ãã¬ã€ã€ãŒãäœããïŒ
MITã©ã€ã»ã³ã¹(FFmpegã¯é忢±)ã§å
¬éããŠããã®ã§ããã£ãã䜿ã£ãŠã¿ãŠãã ããã
P.S.  2åç®ä»¥éã®èµ·åæé(ready-to-show)ã322msã§ã¡ãã£ãšã³ã£ããããã
Discussion