anhuiqiang 1 hete
szülő
commit
cbf68492e6
48 módosított fájl, 873 hozzáadás és 1115 törlés
  1. 5 3
      server/src/admin/admin.service.ts
  2. 3 1
      server/src/api/api-v1.controller.ts
  3. 1 1
      server/src/api/api.controller.ts
  4. 6 5
      server/src/api/api.service.ts
  5. 48 48
      server/src/chat/chat.service.ts
  6. 2 2
      server/src/common/constants.ts
  7. 18 16
      server/src/elasticsearch/elasticsearch.service.ts
  8. 11 11
      server/src/i18n/i18n.service.ts
  9. 82 361
      server/src/i18n/messages.ts
  10. 5 3
      server/src/import-task/import-task.service.ts
  11. 50 50
      server/src/knowledge-base/chunk-config.service.ts
  12. 43 41
      server/src/knowledge-base/embedding.service.ts
  13. 12 12
      server/src/knowledge-base/knowledge-base.controller.ts
  14. 9 9
      server/src/knowledge-base/knowledge-base.entity.ts
  15. 139 139
      server/src/knowledge-base/knowledge-base.service.ts
  16. 46 46
      server/src/knowledge-base/memory-monitor.service.ts
  17. 3 3
      server/src/knowledge-base/text-chunker.service.ts
  18. 6 2
      server/src/knowledge-group/knowledge-group.controller.ts
  19. 2 2
      server/src/libreoffice/libreoffice.interface.ts
  20. 36 32
      server/src/libreoffice/libreoffice.service.ts
  21. 9 9
      server/src/migrations/1737800000000-AddKnowledgeBaseEnhancements.ts
  22. 10 10
      server/src/model-config/dto/create-model-config.dto.ts
  23. 15 15
      server/src/model-config/model-config.entity.ts
  24. 11 10
      server/src/model-config/model-config.service.ts
  25. 8 8
      server/src/note/note-category.service.ts
  26. 4 4
      server/src/ocr/ocr.controller.ts
  27. 7 7
      server/src/ocr/ocr.service.ts
  28. 10 10
      server/src/pdf2image/pdf2image.interface.ts
  29. 26 22
      server/src/pdf2image/pdf2image.service.ts
  30. 5 6
      server/src/podcasts/podcast.service.ts
  31. 32 32
      server/src/rag/rag.service.ts
  32. 11 11
      server/src/rag/rerank.service.ts
  33. 6 2
      server/src/search-history/search-history.controller.ts
  34. 1 1
      server/src/search-history/search-history.service.ts
  35. 3 1
      server/src/tenant/tenant.service.ts
  36. 6 2
      server/src/tika/tika.service.ts
  37. 9 9
      server/src/upload/upload.controller.ts
  38. 5 5
      server/src/upload/upload.module.ts
  39. 6 6
      server/src/upload/upload.service.ts
  40. 4 4
      server/src/user/user.controller.ts
  41. 1 1
      server/src/user/user.entity.ts
  42. 6 6
      server/src/user/user.service.ts
  43. 41 41
      server/src/vision-pipeline/cost-control.service.ts
  44. 46 44
      server/src/vision-pipeline/vision-pipeline-cost-aware.service.ts
  45. 3 3
      server/src/vision-pipeline/vision-pipeline.interface.ts
  46. 3 1
      server/src/vision-pipeline/vision-pipeline.service.ts
  47. 10 10
      server/src/vision/vision.interface.ts
  48. 48 48
      server/src/vision/vision.service.ts

+ 5 - 3
server/src/admin/admin.service.ts

@@ -2,12 +2,14 @@ import { Injectable, NotFoundException, ForbiddenException, BadRequestException
 import * as XLSX from 'xlsx';
 import { UserService } from '../user/user.service';
 import { TenantService } from '../tenant/tenant.service';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class AdminService {
     constructor(
         private readonly userService: UserService,
         private readonly tenantService: TenantService,
+        private readonly i18nService: I18nService,
     ) { }
 
     async getTenantUsers(tenantId?: string, page?: number, limit?: number) {
@@ -37,7 +39,7 @@ export class AdminService {
     }
 
     async importUsers(tenantId?: string, file?: any) {
-        if (!file) throw new BadRequestException('No file uploaded');
+        if (!file) throw new BadRequestException(this.i18nService.getMessage('uploadNoFile'));
 
         const workbook = XLSX.read(file.buffer, { type: 'buffer' });
         const sheetName = workbook.SheetNames[0];
@@ -59,7 +61,7 @@ export class AdminService {
                 const isAdmin = isAdminStr.toLowerCase() === 'yes' || isAdminStr === 'true' || isAdminStr === '1';
 
                 if (!username) {
-                    throw new Error('Username is missing');
+                    throw new Error(this.i18nService.getMessage('usernameRequired'));
                 }
 
                 const existingUser = await this.userService.findOneByUsername(username);
@@ -72,7 +74,7 @@ export class AdminService {
                     });
                 } else {
                     if (!password) {
-                        throw new Error(`Password missing for new user: ${username}`);
+                        throw new Error(this.i18nService.formatMessage('passwordRequiredForNewUser', { username }));
                     }
                     await this.userService.createUser(
                         username,

+ 3 - 1
server/src/api/api-v1.controller.ts

@@ -20,6 +20,7 @@ import { KnowledgeBaseService } from '../knowledge-base/knowledge-base.service';
 import { ModelConfigService } from '../model-config/model-config.service';
 import { TenantService } from '../tenant/tenant.service';
 import { UserSettingService } from '../user/user-setting.service';
+import { I18nService } from '../i18n/i18n.service';
 
 @Controller('v1')
 @UseGuards(ApiKeyGuard)
@@ -31,6 +32,7 @@ export class ApiV1Controller {
         private readonly modelConfigService: ModelConfigService,
         private readonly tenantService: TenantService,
         private readonly userSettingService: UserSettingService,
+        private readonly i18nService: I18nService,
     ) { }
 
     // ========== Chat / RAG ==========
@@ -258,7 +260,7 @@ export class ApiV1Controller {
     async deleteFile(@Request() req, @Param('id') id: string) {
         const user = req.user;
         await this.knowledgeBaseService.deleteFile(id, user.id, user.tenantId);
-        return { message: 'File deleted successfully' };
+        return { message: this.i18nService.getMessage('fileDeleted') };
     }
 
     @Get('knowledge-bases/:id')

+ 1 - 1
server/src/api/api.controller.ts

@@ -47,7 +47,7 @@ export class ApiController {
         throw new Error(this.i18nService.getMessage('addLLMConfig'));
       }
 
-      // APIキーはオプションです - ローカルモデルを許可します
+      // API key is optional - allows local models
 
       // entity タイプを types インターフェースに変換
       const modelConfigForService = {

+ 6 - 5
server/src/api/api.service.ts

@@ -1,12 +1,13 @@
 import { Injectable } from '@nestjs/common';
 import { ChatOpenAI } from '@langchain/openai';
 import { ModelConfig } from '../types';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class ApiService {
-  constructor() { }
+  constructor(private i18nService: I18nService) { }
 
-  // 簡易的なヘルスチェックメソッド
+  // Simple health check method
   healthCheck() {
     return { status: 'ok', message: 'API is healthy' };
   }
@@ -15,7 +16,7 @@ export class ApiService {
     prompt: string,
     modelConfig: ModelConfig,
   ): Promise<string> {
-    // APIキーはオプションです - ローカルモデルを許可します
+    // API key is optional - allows local models
 
     try {
       const llm = this.createLLM(modelConfig);
@@ -24,9 +25,9 @@ export class ApiService {
     } catch (error) {
       console.error('LangChain call failed:', error);
       if (error.message?.includes('401')) {
-        throw new Error('Invalid API key');
+        throw new Error(this.i18nService.getMessage('invalidApiKey'));
       }
-      throw new Error('Failed to get response: ' + error.message);
+      throw new Error(this.i18nService.formatMessage('apiCallFailed', { message: error.message }));
     }
   }
 

+ 48 - 48
server/src/chat/chat.service.ts

@@ -51,19 +51,19 @@ export class ChatService {
     modelConfig: ModelConfig,
     userLanguage: string = DEFAULT_LANGUAGE,
     selectedEmbeddingId?: string,
-    selectedGroups?: string[], // 新規:選択されたグループ
-    selectedFiles?: string[], // 新規:選択されたファイル
-    historyId?: string, // 新規:対話履歴ID
+    selectedGroups?: string[], // New: Selected groups
+    selectedFiles?: string[], // New: Selected files
+    historyId?: string, // New: Chat history ID
     enableRerank: boolean = false,
     selectedRerankId?: string,
-    temperature?: number, // 新規: temperature パラメータ
-    maxTokens?: number, // 新規: maxTokens パラメータ
-    topK?: number, // 新規: topK パラメータ
-    similarityThreshold?: number, // 新規: similarityThreshold パラメータ
-    rerankSimilarityThreshold?: number, // 新規: rerankSimilarityThreshold パラメータ
-    enableQueryExpansion?: boolean, // 新規
-    enableHyDE?: boolean, // 新規
-    tenantId?: string // 新規: tenant isolation
+    temperature?: number, // New: temperature parameter
+    maxTokens?: number, // New: maxTokens parameter
+    topK?: number, // New: topK parameter
+    similarityThreshold?: number, // New: similarityThreshold parameter
+    rerankSimilarityThreshold?: number, // New: rerankSimilarityThreshold parameter
+    enableQueryExpansion?: boolean, // New
+    enableHyDE?: boolean, // New
+    tenantId?: string // New: tenant isolation
   ): AsyncGenerator<{ type: 'content' | 'sources' | 'historyId'; data: any }> {
     console.log('=== ChatService.streamChat ===');
     console.log('ユーザーID:', userId);
@@ -87,19 +87,19 @@ export class ChatService {
     console.log('API Key プレフィックス:', modelConfig.apiKey?.substring(0, 10) + '...');
     console.log('API Key 長さ:', modelConfig.apiKey?.length);
 
-    // 現在の言語設定を取得 (下位互換性のためにLANGUAGE_CONFIGを保持しますが、現在はi18nサービスを使>用)
-    // ユーザー設定に基づいて実際の言語を使用
+    // Get current language setting (keeping LANGUAGE_CONFIG for backward compatibility, now uses i18n service)
+    // Use actual language based on user settings
     const effectiveUserLanguage = userLanguage || DEFAULT_LANGUAGE;
 
     let currentHistoryId = historyId;
     let fullResponse = '';
 
     try {
-      // historyId がない場合は、新しい対話履歴を作成
+      // Create new chat history if no historyId
       if (!currentHistoryId) {
         const searchHistory = await this.searchHistoryService.create(
           userId,
-          tenantId || 'default', // 新規
+          tenantId || 'default', // New
           message,
           selectedGroups,
         );
@@ -108,9 +108,9 @@ export class ChatService {
         yield { type: 'historyId', data: currentHistoryId };
       }
 
-      // ユーザーメッセージを保存
+      // Save user message
       await this.searchHistoryService.addMessage(currentHistoryId, 'user', message);
-      // 1. ユーザーの埋め込みモデル設定を取得
+      // 1. Get user's embedding model settings
       let embeddingModel: any;
 
       if (selectedEmbeddingId) {
@@ -123,7 +123,7 @@ export class ChatService {
 
       console.log(this.i18nService.getMessage('usingEmbeddingModel', effectiveUserLanguage) + embeddingModel.name + ' ' + embeddingModel.modelId + ' ID:' + embeddingModel.id);
 
-      // 2. ユーザーのクエリを直接使用して検索
+      // 2. Search using user's query directly
       console.log(this.i18nService.getMessage('startingSearch', effectiveUserLanguage));
       yield { type: 'content', data: this.i18nService.getMessage('searching', effectiveUserLanguage) + '\n' };
 
@@ -131,14 +131,14 @@ export class ChatService {
       let context = '';
 
       try {
-        // 3. 選択された知識グループがある場合、まずそれらのグループ内のファイルIDを取得
-        let effectiveFileIds = selectedFiles; // 明示的に指定されたファイルを優先
+        // 3. If knowledge groups are selected, get file IDs from those groups first
+        let effectiveFileIds = selectedFiles; // Prioritize explicitly specified files
         if (!effectiveFileIds && selectedGroups && selectedGroups.length > 0) {
-          // ナレッジグループからファイルIDを取得
+          // Get file IDs from knowledge groups
           effectiveFileIds = await this.knowledgeGroupService.getFileIdsByGroups(selectedGroups, userId, tenantId as string);
         }
 
-        // 3. RagService を使用して検索 (混合検索 + Rerank をサポート)
+        // 3. Use RagService for search (supports hybrid search + Rerank)
         const ragResults = await this.ragService.searchKnowledge(
           message,
           userId,
@@ -156,18 +156,18 @@ export class ChatService {
           enableHyDE
         );
 
-        // RagSearchResult を ChatService が必要とする形式 (any[]) に変換
-        // HybridSearch は ES の hit 構造を返しますが、RagSearchResult は正規化されています。
-        // BuildContext は {fileName, content} を期待します。RagSearchResult はこれらを持っています。
+        // Convert RagSearchResult to format needed by ChatService (any[])
+        // HybridSearch returns ES hit structure, but RagSearchResult is normalized
+        // BuildContext expects {fileName, content}. RagSearchResult has these
         searchResults = ragResults;
         console.log(this.i18nService.getMessage('searchResultsCount', effectiveUserLanguage) + searchResults.length);
 
-        // 4. コンテキストの構築
+        // 4. Build context
         context = this.buildContext(searchResults, effectiveUserLanguage);
 
         if (searchResults.length === 0) {
           if (selectedGroups && selectedGroups.length > 0) {
-            // ユーザーがナレッジグループを選択したが、一致するものが見つからなかった場合
+            // User selected knowledge groups but no matches found
             const noMatchMsg = this.i18nService.getMessage('noMatchInKnowledgeGroup', effectiveUserLanguage);
             yield { type: 'content', data: `⚠️ ${noMatchMsg}\n\n` };
           } else {
@@ -180,7 +180,7 @@ export class ChatService {
             type: 'content',
             data: `${searchResults.length} ${this.i18nService.getMessage('relevantInfoFound', effectiveUserLanguage)}。${this.i18nService.getMessage('generatingResponse', effectiveUserLanguage)}...\n\n`,
           };
-          // 一時的なデバッグ情報
+          // Debug info
           const scores = searchResults.map(r => {
             if (r.originalScore !== undefined && r.originalScore !== r.score) {
               return `${r.originalScore.toFixed(2)} → ${r.score.toFixed(2)}`;
@@ -197,7 +197,7 @@ export class ChatService {
         yield { type: 'content', data: this.i18nService.getMessage('searchFailed', effectiveUserLanguage) + '\n\n' };
       }
 
-      // 5. ストリーム回答生成
+      // 5. Stream response generation
       this.logger.log(this.i18nService.formatMessage('modelCall', {
         type: 'LLM',
         model: `${modelConfig.name} (${modelConfig.modelId})`,
@@ -240,7 +240,7 @@ export class ChatService {
         }
       }
 
-      // AI 回答を保存
+      // Save AI response
       await this.searchHistoryService.addMessage(
         currentHistoryId,
         'assistant',
@@ -255,7 +255,7 @@ export class ChatService {
         })),
       );
 
-      // 7. 自動チャットタイトル生成 (最初のやり取りの後に実行)
+      // 7. Auto-generate chat title (executed after first exchange)
       const messagesInHistory = await this.searchHistoryService.findOne(currentHistoryId, userId, tenantId);
       if (messagesInHistory.messages.length === 2) {
         this.generateChatTitle(currentHistoryId, userId, tenantId).catch((err) => {
@@ -263,7 +263,7 @@ export class ChatService {
         });
       }
 
-      // 6. 引用元を返却
+      // 6. Return sources
       yield {
         type: 'sources',
         data: searchResults.map((result) => ({
@@ -302,8 +302,8 @@ export class ChatService {
       });
 
       const systemPrompt = `${this.i18nService.getMessage('intelligentAssistant', 'ja')}
-提供されたテキスト内容を、ユーザーの指示に基づいて修正または改善してください
-挨拶や結びの言葉(「わかりました、こちらが...」など)は含めず、修正後の内容のみを直接出力してください
+提供されたテキスト内容を、ユーザーの指示に基づいて修正または改善please
+挨拶や結びの言葉(「わかりました、こちらが...」etc.)は含めず、修正後の内容のみを直接出力please
 
 コンテキスト(現在の内容):
 ${context}
@@ -328,22 +328,22 @@ ${instruction}`;
     keywords: string[],
     userId: string,
     embeddingModelId?: string,
-    selectedGroups?: string[], // 新規パラメータ
-    explicitFileIds?: string[], // 新規パラメータ
-    tenantId?: string, // 追加
+    selectedGroups?: string[], // New parameter
+    explicitFileIds?: string[], // New parameter
+    tenantId?: string, // Added
   ): Promise<any[]> {
     try {
-      // キーワードを検索文字列に結合
+      // Join keywords into search string
       const combinedQuery = keywords.join(' ');
       console.log(this.i18nService.getMessage('searchString', 'ja') + combinedQuery);
 
-      // 埋め込みモデルIDが提供されているか確認
+      // Check if embedding model ID is provided
       if (!embeddingModelId) {
         console.log(this.i18nService.getMessage('embeddingModelIdNotProvided', 'ja'));
         return [];
       }
 
-      // 実際の埋め込みベクトルを使用
+      // Use actual embedding vector
       console.log(this.i18nService.getMessage('generatingEmbeddings', 'ja'));
       const queryEmbedding = await this.embeddingService.getEmbeddings(
         [combinedQuery],
@@ -353,7 +353,7 @@ ${instruction}`;
       const queryVector = queryEmbedding[0];
       console.log(this.i18nService.getMessage('embeddingsGenerated', 'ja') + this.i18nService.getMessage('dimensions', 'ja') + ':', queryVector.length);
 
-      // 混合検索
+      // Hybrid search
       console.log(this.i18nService.getMessage('performingHybridSearch', 'ja'));
       const results = await this.elasticsearchService.hybridSearch(
         queryVector,
@@ -361,9 +361,9 @@ ${instruction}`;
         userId,
         10,
         0.6,
-        selectedGroups, // 選択されたグループを渡す
-        explicitFileIds, // 明示的なファイルIDを渡す
-        tenantId, // 追加: tenantId
+        selectedGroups, // Pass selected groups
+        explicitFileIds, // Pass explicit file IDs
+        tenantId, // Added: tenantId
       );
       console.log(this.i18nService.getMessage('esSearchCompleted', 'ja') + this.i18nService.getMessage('resultsCount', 'ja') + ':', results.length);
 
@@ -478,14 +478,14 @@ ${instruction}`;
         return null;
       }
 
-      // ユーザ設定から言語を取得
+      // Get language from user settings
       const userSettings = await this.userSettingService.getByUser(userId);
       const language = userSettings?.language || 'ja';
 
-      // プロンプトを構築
+      // Build prompt
       const prompt = this.i18nService.getChatTitlePrompt(language, userMessage, aiResponse);
 
-      // LLMを呼び出してタイトルを生成
+      // Call LLM to generate title
       const generatedTitle = await this.generateSimpleChat(
         [{ role: 'user', content: prompt }],
         userId,
@@ -493,7 +493,7 @@ ${instruction}`;
       );
 
       if (generatedTitle && generatedTitle.trim().length > 0) {
-        // 余分な引用符を除去
+        // Remove extra quotes
         const cleanedTitle = generatedTitle.trim().replace(/^["']|["']$/g, '').substring(0, 50);
         await this.searchHistoryService.updateTitle(historyId, cleanedTitle);
         this.logger.log(`Successfully generated title for chat ${historyId}: ${cleanedTitle}`);

+ 2 - 2
server/src/common/constants.ts

@@ -13,7 +13,7 @@ export const DEFAULT_MAX_OVERLAP_RATIO = 0.5;
 // ベクトル次元のデフォルト値 (OpenAI Standard)
 export const DEFAULT_VECTOR_DIMENSIONS = 1536;
 
-// ファイルサイズの制限 (バイト)
+// File size limit (バイト)
 export const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
 
 // バッチ処理の制限
@@ -22,5 +22,5 @@ export const DEFAULT_MAX_BATCH_SIZE = 2048;
 // デフォルト言語
 export const DEFAULT_LANGUAGE = 'ja';
 
-// システム全体の共通テナントID(シードデータなどで使用)
+// システム全体の共通テナントID(シードデータetc.で使用)
 export const GLOBAL_TENANT_ID = '00000000-0000-0000-0000-000000000000';

+ 18 - 16
server/src/elasticsearch/elasticsearch.service.ts

@@ -2,6 +2,7 @@
 import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
 import { ConfigService } from '@nestjs/config';
 import { Client } from '@elastic/elasticsearch';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class ElasticsearchService implements OnModuleInit {
@@ -11,6 +12,7 @@ export class ElasticsearchService implements OnModuleInit {
 
   constructor(
     private configService: ConfigService,
+    private i18nService: I18nService,
   ) {
     const node = this.configService.get<string>('ELASTICSEARCH_HOST'); // Changed from NODE to HOST
     this.indexName = this.configService.get<string>(
@@ -19,7 +21,7 @@ export class ElasticsearchService implements OnModuleInit {
     );
 
     if (!node) {
-      throw new Error('ELASTICSEARCH_HOST environment variable not set.');
+      throw new Error(this.i18nService.getMessage('elasticsearchHostRequired'));
     }
 
     this.client = new Client({
@@ -31,7 +33,7 @@ export class ElasticsearchService implements OnModuleInit {
     try {
       const health = await this.client.cluster.health();
       this.logger.log(`Elasticsearch cluster health is: ${health.status}`);
-      // 初期化時にはインデックスを作成せず、実際の使用時にモデルに基づいて動的に作成されるのを待つ
+      // Index is created dynamically on first use based on the model
     } catch (error) {
       this.logger.error('Failed to connect to Elasticsearch', error);
     }
@@ -48,7 +50,7 @@ export class ElasticsearchService implements OnModuleInit {
       );
       await this.createIndex(vectorDimensions);
     } else {
-      // 既存インデックスのベクトル次元数を確認
+      // Check existing index vector dimensions
       const mapping = await this.client.indices.getMapping({
         index: this.indexName,
       });
@@ -63,7 +65,7 @@ export class ElasticsearchService implements OnModuleInit {
         );
         this.logger.warn(`原因: 異なる次元数の埋め込みモデルに変更された可能性があります。システムは自動的にインデックスを再作成します。`);
 
-        // 既存インデックスを削除して再作成
+        // Delete existing index and recreate
         await this.client.indices.delete({ index: this.indexName });
         this.logger.log(`旧インデックスを正常に削除しました: ${this.indexName}`);
 
@@ -89,7 +91,7 @@ export class ElasticsearchService implements OnModuleInit {
 
     if (!vector || vector.length === 0) {
       this.logger.error(`Invalid vector for document ${documentId}`);
-      throw new Error('Vector is required for indexing');
+      throw new Error(this.i18nService.getMessage('vectorRequired'));
     }
 
     const document = {
@@ -150,7 +152,7 @@ export class ElasticsearchService implements OnModuleInit {
         source: 'ctx._source.title = params.title',
         params: { title },
       },
-      refresh: true, // 即座に検索に反映させる
+      refresh: true, // Reflect in search immediately
     });
   }
 
@@ -200,7 +202,7 @@ export class ElasticsearchService implements OnModuleInit {
 
       const results = response.hits.hits.map((hit: any) => ({
         id: hit._id,
-        score: this.normalizeScore(hit._score), // スコアの正規化
+        score: this.normalizeScore(hit._score), // Normalize score
         content: hit._source?.content,
         fileId: hit._source?.fileId,
         fileName: hit._source?.fileName,
@@ -261,7 +263,7 @@ export class ElasticsearchService implements OnModuleInit {
 
       const results = response.hits.hits.map((hit: any) => ({
         id: hit._id,
-        score: this.normalizeScore(hit._score), // スコアの正規化
+        score: this.normalizeScore(hit._score), // Normalize score
         content: hit._source?.content,
         fileId: hit._source?.fileId,
         fileName: hit._source?.fileName,
@@ -287,15 +289,15 @@ export class ElasticsearchService implements OnModuleInit {
     userId: string,
     topK: number = 5,
     vectorWeight: number = 0.7,
-    selectedGroups?: string[], // 後方互換性のために残す(未使用)
-    explicitFileIds?: string[], // 明示的に指定されたファイルIDリスト
+    selectedGroups?: string[], // Keep for backward compatibility(未使用)
+    explicitFileIds?: string[], // Explicitly specified file ID list
     tenantId?: string,
   ) {
-    // selectedGroups は廃止予定。呼び出し側で fileIds に変換して explicitFileIds を使用してください
+    // selectedGroups is deprecated。呼び出し側で fileIds に変換して explicitFileIds を使用please
     const fileIds = explicitFileIds;
 
     if (fileIds && fileIds.length === 0) {
-      this.logger.log('検索対象ファイルが0件のため、検索をスキップします');
+      this.logger.log('検索対象ファイルが0件for、検索をスキップします');
       return [];
     }
 
@@ -309,10 +311,10 @@ export class ElasticsearchService implements OnModuleInit {
       this.searchFullTextWithFileFilter(query, userId, topK, fileIds, tenantId),
     ]);
 
-    // 結果をマージして重複を排除
+    // Merge results and remove duplicates
     const combinedResults = new Map();
 
-    // 向量搜索結果を追加
+    // Add vector search results
     vectorResults.forEach((result) => {
       combinedResults.set(result.id, {
         ...result,
@@ -322,7 +324,7 @@ export class ElasticsearchService implements OnModuleInit {
       });
     });
 
-    // 全文検索結果を追加
+    // Add full-text search results
     textResults.forEach((result) => {
       if (combinedResults.has(result.id)) {
         const existing = combinedResults.get(result.id);
@@ -340,7 +342,7 @@ export class ElasticsearchService implements OnModuleInit {
       }
     });
 
-    // 正規化のためにすべての組み合わせスコアを取得
+    // 正規化forにすべての組み合わせスコアを取得
     const allScores = Array.from(combinedResults.values()).map(r => r.combinedScore);
     const maxScore = Math.max(...allScores, 1); // ゼロ除算を避けるため最小1
     const minScore = Math.min(...allScores);

+ 11 - 11
server/src/i18n/i18n.service.ts

@@ -178,9 +178,9 @@ Please answer in English.
 `;
     } else { // 默认为日语,符合项目要求
       return type === 'withContext' ? `
-以下のナレッジベースの内容に基づいてユーザーの質問に答えてください
+以下のナレッジベースの内容に基づいてユーザーの質問に答えてくplease
 ${hasKnowledgeGroup ? `
-**重要**: ユーザーが特定の知識グループを選択しました。以下のナレッジベースの内容に厳密に基づいて回答してください。ナレッジベースに関連情報がない場合は、「${noMatchMsg}」とユーザーに明示的に伝えてから、回答を提供してください
+**重要**: ユーザーが特定の知識グループを選択しました。以下のナレッジベースの内容に厳密に基づいて回答please。ナレッジベースに関連情報がない場合は、「${noMatchMsg}」とユーザーに明示的に伝えてfrom、回答を提供please
 ` : ''}
 ナレッジベースの内容:
 {context}
@@ -190,7 +190,7 @@ ${hasKnowledgeGroup ? `
 
 ユーザーの質問:{question}
 
-日本語で回答してください。以下の Markdown 書式要件に厳密に従ってください
+日本語で回答please。以下の Markdown 書式要件に厳密に従ってくplease
 
 1. **段落と構造**:
    - 明確な段落分けを使用し、要点間に空行を入れる
@@ -207,10 +207,10 @@ ${hasKnowledgeGroup ? `
      def example():
          return "例"
      \`\`\`
-   - 対応言語:python, javascript, typescript, java, bash, sql など
+   - 対応言語:python, javascript, typescript, java, bash, sql etc.
 
 4. **図表とチャート**:
-   - フローチャート、シーケンス図などに Mermaid 構文を使用:
+   - フローチャート、シーケンス図etc.に Mermaid 構文を使用:
      \`\`\`mermaid
      graph LR
          A[開始] --> B[処理]
@@ -223,13 +223,13 @@ ${hasKnowledgeGroup ? `
    - 複数のステップがある場合は番号付きリストを使用
    - 比較情報には表を使用(該当する場合)
 ` : `
-インテリジェントアシスタントとして、ユーザーの質問に答えてください
+インテリジェントアシスタントとして、ユーザーの質問に答えてくplease
 
 会話履歴:
 {history}
 
 ユーザーの質問:{question}
-      日本語で回答してください
+      日本語で回答please
 `;
     }
   }
@@ -250,8 +250,8 @@ Language: English
 Text:
 ${contentSample}`;
     } else {
-      return `あなたはドキュメントアナライザーです。以下のテキスト(ドキュメントの冒頭部分)を読み、簡潔でプロフェッショナルなタイトル(最大50文字)を生成してください
-タイトルテキストのみを返してください。説明文や前置き(例:「タイトルは:」)は含めないでください
+      return `あなたはドキュメントアナライザー.以下のテキスト(ドキュメントの冒頭部分)を読み、簡潔でプロフェッショナルなタイトル(最大50文字)を生成please
+タイトルテキストのみを返please。説明文や前置き(例:「タイトルは:」)は含めないでくplease
 言語:日本語
 テキスト:
 ${contentSample}`;
@@ -275,8 +275,8 @@ Snippet:
 User: ${userMessage}
 Assistant: ${aiResponse}`;
     } else {
-      return `以下の会話スニペットに基づいて、トピックを要約する短く説明的なタイトル(最大50文字)を生成してください
-タイトルのみを返してください。前置きは不要です。
+      return `以下の会話スニペットに基づいて、トピックを要約する短く説明的なタイトル(最大50文字)を生成please
+タイトルのみを返please。前置きは不要.
 言語:日本語
 スニペット:
 ユーザー: ${userMessage}

+ 82 - 361
server/src/i18n/messages.ts

@@ -57,11 +57,28 @@ export const errorMessages = {
     retryMechanismError: '重试机制异常',
     imageLoadError: '无法读取图像: {message}',
     groupNotFound: '分组不存在',
+    fileDeleted: '文件删除成功',
+    fileDeletedFromGroup: '文件从分组中移除成功',
+    kbCleared: '知识库清除成功',
+    groupSyncSuccess: '分组同步成功',
+    groupDeleted: '分组删除成功',
+    searchHistoryDeleted: '搜索历史删除成功',
+    jwtSecretRequired: 'JWT_SECRET 环境变量未设置',
+    tenantNotFound: '租户不存在',
+    usernameRequired: '用户名是必填项',
+    passwordRequiredForNewUser: '新用户 {username} 需要密码',
+    importTaskNotFound: '导入任务不存在',
+    sourcePathNotFound: '源路径不存在: {path}',
+    targetGroupRequired: '未指定目标分组',
+    modelConfigNotFound: '找不到模型配置: {id}',
+    cannotUpdateOtherTenantModel: '无法更新其他租户的模型',
+    cannotDeleteOtherTenantModel: '无法删除其他租户的模型',
+    elasticsearchHostRequired: 'ELASTICSEARCH_HOST 环境变量未设置',
   },
   ja: {
-    noEmbeddingModel: '先にシステム設定で埋め込みモデルを設定してください',
+    noEmbeddingModel: '先にシステム設定で埋め込みモデルを設定please',
     searchFailed: 'ナレッジベース検索に失敗しました。一般的な知識に基づいて回答します...',
-    invalidApiKey: 'APIキーが無効です',
+    invalidApiKey: 'APIキーが無効is',
     fileNotFound: 'ファイルが見つかりません',
     insufficientQuota: '利用枠が不足しています',
     modelNotConfigured: 'モデルが設定されていません',
@@ -70,23 +87,23 @@ export const errorMessages = {
     uploadNoFile: 'ファイルがアップロードされていません',
     uploadSizeExceeded: 'ファイルサイズが制限を超えています: {size}, 最大許容: {max}',
     uploadModelRequired: '埋め込みモデルを選択する必要があります',
-    uploadTypeUnsupported: 'サポートされていないファイル形式です: {type}',
+    uploadTypeUnsupported: 'サポートされていないファイル形式is: {type}',
     chunkOverflow: 'チャンクサイズ {size} が上限 {max} ({reason}) を超えています。自動調整されました',
-    chunkUnderflow: 'チャンクサイズ {size} が最小値 {min} 未満です。自動調整されました',
+    chunkUnderflow: 'チャンクサイズ {size} が最小値 {min} 未満.自動調整されました',
     overlapOverflow: '重なりサイズ {size} が上限 {max} を超えています。自動調整されました',
-    overlapUnderflow: '重なりサイズ {size} が最小値 {min} 未満です。自動調整されました',
+    overlapUnderflow: '重なりサイズ {size} が最小値 {min} 未満.自動調整されました',
     overlapRatioExceeded: '重なりサイズ {size} がチャンクサイズの50% ({max}) を超えています。自動調整されました',
     batchOverflowWarning: 'バッチ処理のオーバーフローを避けるため、チャンクサイズを {safeSize} 以下にすることをお勧めします (現在: {size}, モデル制限の {percent}%)',
     estimatedChunkCountExcessive: '推定チャンク数が多すぎます ({count})。処理に時間がかかる可能性があります',
-    contentAndTitleRequired: '内容とタイトルは必須です',
+    contentAndTitleRequired: '内容とタイトルは必須is',
     embeddingModelNotFound: '埋め込みモデル {id} が見つかりません、またはタイプが embedding ではありません',
     ocrFailed: 'テキストの抽出に失敗しました: {message}',
     noImageUploaded: '画像がアップロードされていません',
     adminOnlyViewList: '管理者のみがユーザーリストを表示できます',
-    passwordsRequired: '現在のパスワードと新しいパスワードは必須です',
+    passwordsRequired: '現在のパスワードと新しいパスワードは必須is',
     newPasswordMinLength: '新しいパスワードは少なくとも6文字以上である必要があります',
     adminOnlyCreateUser: '管理者のみがユーザーを作成できます',
-    usernamePasswordRequired: 'ユーザー名とパスワードは必須です',
+    usernamePasswordRequired: 'ユーザー名とパスワードは必須is',
     passwordMinLength: 'パスワードは少なくとも6文字以上である必要があります',
     adminOnlyUpdateUser: '管理者のみがユーザー情報を更新できます',
     userNotFound: 'ユーザーが見つかりません',
@@ -100,22 +117,39 @@ export const errorMessages = {
     noteNotFound: 'ノートが見つかりません: {id}',
     knowledgeGroupNotFound: 'ナレッジグループが見つかりません: {id}',
     accessDeniedNoToken: 'アクセス不許可:トークンがありません',
-    invalidToken: '無効なトークンです',
+    invalidToken: '無効なトークンis',
     pdfFileNotFound: 'PDF ファイルが見つかりません',
-    pdfFileEmpty: 'PDF ファイルが空です。変換に失敗した可能性があります',
+    pdfFileEmpty: 'PDF ファイルが空.変換に失敗した可能性があります',
     pdfConversionFailed: 'PDF ファイルが存在しないか、変換に失敗しました',
-    pdfConversionFailedDetail: 'PDF 変換に失敗しました(ファイル ID: {id})。後でもう一度お試しください',
+    pdfConversionFailedDetail: 'PDF 変換に失敗しました(ファイル ID: {id})。後でもう一度お試しくplease',
     pdfPreviewNotSupported: 'このファイル形式はプレビューをサポートしていません',
     pdfServiceUnavailable: 'PDF サービスを利用できません: {message}',
     pageImageNotFound: 'ページ画像が見つかりません',
     pdfPageImageFailed: 'PDF ページの画像を取得できませんでした',
     someGroupsNotFound: '一部のグループが存在しません',
-    promptRequired: 'プロンプトは必須です',
-    addLLMConfig: 'システム設定で LLM モデルを追加してください',
+    promptRequired: 'プロンプトは必須is',
+    addLLMConfig: 'システム設定で LLM モデルを追加please',
     visionAnalysisFailed: 'ビジョン分析に失敗しました: {message}',
     retryMechanismError: '再試行メカニズムの異常',
     imageLoadError: '画像を読み込めません: {message}',
     groupNotFound: 'グループが存在しません',
+    fileDeleted: 'ファイルが削除されました',
+    fileDeletedFromGroup: 'ファイルがグループから削除されました',
+    kbCleared: 'ナレッジベースがクリアされました',
+    groupSyncSuccess: 'グループ同期が完了しました',
+    groupDeleted: 'グループが削除されました',
+    searchHistoryDeleted: '検索履歴が削除されました',
+    jwtSecretRequired: 'JWT_SECRET 環境変数が設定されていません',
+    tenantNotFound: 'テナントが見つかりません',
+    usernameRequired: 'ユーザー名は必須is',
+    passwordRequiredForNewUser: '新しいユーザー {username} のパスワードが必要is',
+    importTaskNotFound: 'インポートタスクが見つかりません',
+    sourcePathNotFound: 'ソースパスが見つかりません: {path}',
+    targetGroupRequired: 'ターゲットグループが指定されていません',
+    modelConfigNotFound: 'モデル設定が見つかりません: {id}',
+    cannotUpdateOtherTenantModel: '他のテナントのモデルは更新できません',
+    cannotDeleteOtherTenantModel: '他のテナントのモデルは削除できません',
+    elasticsearchHostRequired: 'ELASTICSEARCH_HOST 環境変数が設定されていません',
   },
   en: {
     noEmbeddingModel: 'Please configure embedding model in system settings first',
@@ -146,6 +180,7 @@ export const errorMessages = {
     newPasswordMinLength: 'New password must be at least 6 characters',
     adminOnlyCreateUser: 'Only admins can create users',
     usernamePasswordRequired: 'Username and password are required',
+    usernameRequired: 'Username is required',
     passwordMinLength: 'Password must be at least 6 characters',
     adminOnlyUpdateUser: 'Only admins can update user info',
     userNotFound: 'User not found',
@@ -153,7 +188,6 @@ export const errorMessages = {
     adminOnlyDeleteUser: 'Only admins can delete users',
     cannotDeleteSelf: 'Cannot delete your own account',
     cannotDeleteBuiltinAdmin: 'Cannot delete built-in admin account',
-    onlyBuiltinAdminCanChangeRole: 'Only built-in admin can change user roles',
     incorrectCredentials: 'Incorrect username or password',
     incorrectCurrentPassword: 'Incorrect current password',
     usernameExists: 'Username already exists',
@@ -176,363 +210,50 @@ export const errorMessages = {
     retryMechanismError: 'Retry mechanism error',
     imageLoadError: 'Cannot load image: {message}',
     groupNotFound: 'Group not found',
+    fileDeleted: 'File deleted successfully',
+    fileDeletedFromGroup: 'File removed from group successfully',
+    kbCleared: 'Knowledge base cleared successfully',
+    groupSyncSuccess: 'Group sync completed successfully',
+    groupDeleted: 'Group deleted successfully',
+    searchHistoryDeleted: 'Search history deleted successfully',
+    jwtSecretRequired: 'JWT_SECRET environment variable is required but not set',
+    tenantNotFound: 'Tenant not found',
+    importTaskNotFound: 'Import task not found',
+    sourcePathNotFound: 'Source path not found: {path}',
+    targetGroupRequired: 'Target group not specified',
+    modelConfigNotFound: 'Model config not found: {id}',
+    cannotUpdateOtherTenantModel: 'Cannot update models from another tenant',
+    cannotDeleteOtherTenantModel: 'Cannot delete models from another tenant',
+    elasticsearchHostRequired: 'ELASTICSEARCH_HOST environment variable is not set',
+    libreofficeUrlRequired: 'LIBREOFFICE_URL environment variable is required but not set',
+    pdfToImageConversionFailed: 'PDF to image conversion failed. No images were generated.',
+    pdfPageCountError: 'Could not get PDF page count',
+    parentCategoryNotFound: 'Parent category not found',
+    categoryNotFound: 'Category not found',
+    maxCategoryDepthExceeded: 'Maximum category depth (3 levels) exceeded',
+    userIdRequired: 'User ID is required',
+    podcastNotFound: 'Podcast not found: {id}',
+    scriptGenerationFailed: 'Script generation failed to produce valid JSON',
+    vectorRequired: 'Vector is required for indexing',
+    apiCallFailed: 'API call failed: {message}',
+    tikaHostRequired: 'TIKA_HOST environment variable is required but not set',
   }
 };
 
 export const logMessages = {
-  zh: {
-    processingFile: '处理文件: {name} ({size})',
-    indexingComplete: '索引完成: {id}',
-    vectorizingFile: '向量化文件: ',
-    searchQuery: '搜索查询: ',
-    modelCall: '[模型调用] 类型: {type}, 模型: {model}, 用户: {user}',
-    memoryStatus: '内存状态: ',
-    uploadSuccess: '文件上传成功。正在后台索引',
-    overlapAdjusted: '重叠大小超过切片大小的50%。已自动调整为 {newSize}',
-    environmentLimit: '环境变量限制',
-    modelLimit: '模型限制',
-    configLoaded: '数据库模型配置加载: {name} ({id})',
-    batchSizeAdjusted: '批量大小从 {old} 调整为 {new} (模型限制: {limit})',
-    dimensionMismatch: '模型 {id} 维度不匹配: 预期 {expected}, 实际 {actual}',
-    searchMetadataFailed: '为用户 {userId} 搜索知识库失败',
-    extractedTextTooLarge: '抽出されたテキストが大きいです: {size}MB',
-    preciseModeUnsupported: '格式 {ext} 不支持精密模式,回退到快速模式',
-    visionModelNotConfiguredFallback: '未配置视觉模型,回退到快速模式',
-    visionModelInvalidFallback: '视觉模型配置无效,回退到快速模式',
-    visionPipelineFailed: '视觉流水线失败,回退到快速模式',
-    preciseModeComplete: '精密模式提取完成: {pages}页, 费用: ${cost}',
-    skippingEmptyVectorPage: '跳过第 {page} 页(空向量)',
-    pdfPageImageError: '获取 PDF 页面图像失败: {message}',
-    internalServerError: '服务器内部错误',
-  },
-  ja: {
-    processingFile: 'ファイル処理中: {name} ({size})',
-    indexingComplete: 'インデックス完了: {id}',
-    vectorizingFile: 'ファイルベクトル化中: ',
-    searchQuery: '検索クエリ: ',
-    modelCall: '[モデル呼び出し] タイプ: {type}, モデル: {model}, ユーザー: {user}',
-    memoryStatus: 'メモリ状態: ',
-    uploadSuccess: 'ファイルが正常にアップロードされました。バックグラウンドでインデックス処理を実行中です',
-    overlapAdjusted: 'オーバーラップサイズがチャンクサイズの50%を超えています。自動的に {newSize} に調整されました',
-    environmentLimit: '環境変数の制限',
-    modelLimit: 'モデルの制限',
-    configLoaded: 'データベースからモデル設定を読み込みました: {name} ({id})',
-    batchSizeAdjusted: 'バッチサイズを {old} から {new} に調整しました (モデル制限: {limit})',
-    dimensionMismatch: 'モデル {id} の次元が一致しません: 期待値 {expected}, 実際 {actual}',
-    searchMetadataFailed: 'ユーザー {userId} のナレッジベース検索に失敗しました',
-    extractedTextTooLarge: '抽出されたテキストが大きいです: {size}MB',
-    preciseModeUnsupported: 'ファイル形式 {ext} は精密モードをサポートしていません。高速モードにフォールバックします',
-    visionModelNotConfiguredFallback: 'ビジョンモデルが設定されていません。高速モードにフォールバックします',
-    visionModelInvalidFallback: 'ビジョンモデルの設定が無効です。高速モードにフォールバックします',
-    visionPipelineFailed: 'ビジョンパイプラインが失敗しました。高速モードにフォールバックします',
-    preciseModeComplete: '精密モード内容抽出完了: {pages}ページ, コスト: ${cost}',
-    skippingEmptyVectorPage: '第 {page} ページの空ベクトルをスキップします',
-    pdfPageImageError: 'PDF ページの画像取得に失敗しました: {message}',
-    internalServerError: 'サーバー内部エラー',
-  },
-  en: {
-    processingFile: 'Processing file: {name} ({size})',
-    indexingComplete: 'Indexing complete: {id}',
-    vectorizingFile: 'Vectorizing file: ',
-    searchQuery: 'Search query: ',
-    modelCall: '[Model call] Type: {type}, Model: {model}, User: {user}',
-    memoryStatus: 'Memory status: ',
-    uploadSuccess: 'File uploaded successfully. Indexing in background',
-    overlapAdjusted: 'Overlap size exceeds 50% of chunk size. Auto-adjusted to {newSize}',
-    environmentLimit: 'Environment variable limit',
-    modelLimit: 'Model limit',
-    configLoaded: 'Model config loaded from DB: {name} ({id})',
-    batchSizeAdjusted: 'Batch size adjusted from {old} to {new} (Model limit: {limit})',
-    dimensionMismatch: 'Model {id} dimension mismatch: Expected {expected}, Actual {actual}',
-    searchMetadataFailed: 'Failed to search knowledge base for user {userId}',
-    extractedTextTooLarge: 'Extracted text is too large: {size}MB',
-    preciseModeUnsupported: 'Format {ext} not supported for precise mode. Falling back to fast mode',
-    visionModelNotConfiguredFallback: 'Vision model not configured. Falling back to fast mode',
-    visionModelInvalidFallback: 'Vision model config invalid. Falling back to fast mode',
-    visionPipelineFailed: 'Vision pipeline failed. Falling back to fast mode',
-    preciseModeComplete: 'Precise mode extraction complete: {pages} pages, cost: ${cost}',
-    skippingEmptyVectorPage: 'Skipping page {page} due to empty vector',
-    pdfPageImageError: 'Failed to retrieve PDF page image: {message}',
-    internalServerError: 'Internal server error',
-  }
+  zh: {},
+  ja: {},
+  en: {},
 };
 
 export const statusMessages = {
   zh: {
-    searching: '正在搜索知识库...',
-    noResults: '未找到相关知识,将基于一般知识回答...',
-    searchFailed: '知识库搜索失败,将基于一般知识回答...',
-    generatingResponse: '正在生成回答',
-    files: '个文件',
-    notebooks: '个笔记本',
-    all: '全部',
-    items: '个',
-    searchResults: '搜索结果',
-    relevantInfoFound: '条相关信息找到',
-    searchHits: '搜索命中',
-    relevance: '相关度',
-    sourceFiles: '源文件',
-    searchScope: '搜索范围',
-    error: '错误',
-    creatingHistory: '创建新对话历史: ',
-    searchingModelById: '根据ID搜索模型: ',
-    searchModelFallback: '未找到指定的嵌入模型。使用第一个可用模型。',
-    noEmbeddingModelFound: '找不到嵌入模型设置',
-    usingEmbeddingModel: '使用的嵌入模型: ',
-    startingSearch: '开始搜索知识库...',
-    searchResultsCount: '搜索结果数: ',
-    searchFailedLog: '搜索失败',
-    modelCall: '[模型调用]',
-    chatStreamError: '聊天流错误',
-    assistStreamError: '辅助流错误',
-    file: '文件',
-    content: '内容',
-    userLabel: '用户',
-    assistantLabel: '助手',
-    intelligentAssistant: '您是智能写作助手。',
-    searchString: '搜索字符串: ',
-    embeddingModelIdNotProvided: '未提供嵌入模型ID',
-    generatingEmbeddings: '生成嵌入向量...',
-    embeddingsGenerated: '嵌入向量生成完成',
-    dimensions: '维度',
-    performingHybridSearch: '执行混合搜索...',
-    esSearchCompleted: 'ES搜索完成',
-    resultsCount: '结果数',
-    hybridSearchFailed: '混合搜索失败',
-    getContextForTopicFailed: '获取主题上下文失败',
-    noLLMConfigured: '用户未配置LLM模型',
-    simpleChatGenerationError: '简单聊天生成错误',
-    noMatchInKnowledgeGroup: '所选知识组中未找到相关内容,以下是基于模型的一般性回答:',
-    uploadTextSuccess: '笔记内容已接收。正在后台索引',
-    passwordChanged: '密码已成功修改',
-    userCreated: '用户已成功创建',
-    userInfoUpdated: '用户信息已更新',
-    userDeleted: '用户已删除',
-    pdfNoteTitle: 'PDF 笔记 - {date}',
-    noTextExtracted: '未提取到文本',
-    kbCleared: '知识库已清空',
-    fileDeleted: '文件已删除',
-    pageImageNotFoundDetail: '无法获取 PDF 第 {page} 页’的图像',
-    groupSyncSuccess: '文件分组已更新',
-    fileDeletedFromGroup: '文件已从分组中删除',
-    chunkConfigCorrection: '切片配置已修正: {warnings}',
-    noChunksGenerated: '文件 {id} 未生成任何切片',
-    chunkCountAnomaly: '实际切片数 {actual} 大幅超过预计值 {estimated},可能存在异常',
-    batchSizeExceeded: '批次 {index} 的大小 {actual} 超过推荐值 {limit},将拆分处理',
-    skippingEmptyVectorChunk: '跳过文本块 {index} (空向量)',
-    contextLengthErrorFallback: '批次处理发生上下文长度错误,降级到逐条处理模式',
-    chunkLimitExceededForceBatch: '切片数 {actual} 超过模型批次限制 {limit},强制进行批次处理',
-    noteContentRequired: '笔记内容是必填项',
-    imageAnalysisStarted: '正在使用模型 {id} 分析图像...',
-    batchAnalysisStarted: '正在分析 {count} 张图像...',
-    pageAnalysisFailed: '第 {page} 页分析失败',
-    visionSystemPrompt: '您是专业的文档分析助手。请分析此文档图像,并按以下要求以 JSON 格式返回:\n\n1. 提取所有可读文本(按阅读顺序,保持段落和格式)\n2. 识别图像/图表/表格(描述内容、含义和作用)\n3. 分析页面布局(仅文本/文本和图像混合/表格/图表等)\n4. 评估分析质量 (0-1)\n\n响应格式:\n{\n  "text": "完整的文本内容",\n  "images": [\n    {"type": "图表类型", "description": "详细描述", "position": 1}\n  ],\n  "layout": "布局说明",\n  "confidence": 0.95\n}',
-    visionModelCall: '[模型调用] 类型: Vision, 模型: {model}, 页面: {page}',
-    visionAnalysisSuccess: '✅ 视觉分析完成: {path}{page}, 文本长度: {textLen}, 图像数: {imgCount}, 布局: {layout}, 置信度: {confidence}%',
-    conversationHistoryNotFound: '对话历史不存在',
-    batchContextLengthErrorFallback: '小文件批次处理发生上下文长度错误,降级到逐条处理模式',
-    chunkProcessingFailed: '处理文本块 {index} 失败,已跳过: {message}',
-    singleTextProcessingComplete: '逐条文本处理完成: {count} 个切片',
-    fileVectorizationComplete: '文件 {id} 向量化完成。共处理 {count} 个文本块。最终内存: {memory}MB',
-    fileVectorizationFailed: '文件 {id} 向量化失败',
-    batchProcessingStarted: '开始批次处理: {count} 个项目',
-    batchProcessingProgress: '正在处理批次 {index}/{total}: {count} 个项目',
-    batchProcessingComplete: '批次处理完成: {count} 个项目,耗时 {duration}s',
-    onlyFailedFilesRetryable: '仅允许重试失败的文件 (当前状态: {status})',
-    emptyFileRetryFailed: '文件内容为空,无法重试。请重新上传文件。',
-    ragSystemPrompt: '您是专业的知识库助手。请根据以下提供的文档内容回答用户的问题。',
-    ragRules: '## 规则:\n1. 仅根据提供的文档内容进行回答,请勿编造信息。\n2. 如果文档中没有相关信息,请告知用户。\n3. 请在回答中注明信息来源。格式:[文件名.扩展子]\n4. 如果多个文档中的信息存在矛盾,请进行综合分析或解释不同的观点。\n5. 请使用{lang}进行回答。',
-    ragDocumentContent: '## 文档内容:',
-    ragUserQuestion: '## 用户问题:',
-    ragAnswer: '## 回答:',
-    ragSource: '### 来源:{fileName}',
-    ragSegment: '片段 {index} (相似度: {score}):',
-    ragNoDocumentFound: '未找到相关文档。',
-    queryExpansionPrompt: '您是一个搜索助手。请为以下用户查询生成3个不同的演变版本,以帮助在向量搜索中获得更好的结果。每个版本应包含不同的关键词或表达方式,但保持原始意思。直接输出3行查询,不要有数字或编号:\n\n查询:{query}',
-    hydePrompt: '请为以下用户问题写一段简短、事实性的假设回答(约100字)。不要包含任何引导性文字(如“基于我的分析...”),直接输出答案内容。\n\n问题:{query}',
+    noMatchInKnowledgeGroup: '所选知识组中未找到相关内容',
   },
   ja: {
-    searching: 'ナレッジベースを検索中...',
-    noResults: '関連する知識が見つかりませんでした。一般的な知識に基づいて回答します...',
-    searchFailed: 'ナレッジベース検索に失敗しました。一般的な知識に基づいて回答します...',
-    generatingResponse: '回答を生成中',
-    files: '個のファイル',
-    notebooks: '個のノートブック',
-    all: 'すべて',
-    items: '件',
-    searchResults: '検索結果',
-    relevantInfoFound: '件の関連情報が見つかりました',
-    searchHits: '検索ヒット',
-    relevance: '関連度',
-    sourceFiles: '元ファイル',
-    searchScope: '検索範囲',
-    error: 'エラー',
-    creatingHistory: '新規対話履歴を作成: ',
-    searchingModelById: 'selectedEmbeddingId に基づいてモデルを検索: ',
-    searchModelFallback: '指定された埋め込みモデルが見つかりません。最初に使用可能なモデルを使用します。',
-    noEmbeddingModelFound: '埋め込みモデルの設定が見つかりません',
-    usingEmbeddingModel: '使用する埋め込みモデル: ',
-    startingSearch: 'ナレッジベースの検索を開始...',
-    searchResultsCount: '検索結果数: ',
-    searchFailedLog: '検索失敗',
-    chatStreamError: 'チャットストリームエラー',
-    assistStreamError: 'アシストストリームエラー',
-    file: 'ファイル',
-    content: '内容',
-    userLabel: 'ユーザー',
-    assistantLabel: 'アシスタント',
-    intelligentAssistant: 'あなたはインテリジェントな執筆アシスタントです。',
-    searchString: '検索文字列: ',
-    embeddingModelIdNotProvided: '埋め込みモデルIDが提供されていません',
-    generatingEmbeddings: '埋め込みベクトルを生成中...',
-    embeddingsGenerated: '埋め込みベクトルの生成が完了しました',
-    dimensions: '次元数',
-    performingHybridSearch: 'ES 混合検索を実行中...',
-    esSearchCompleted: 'ES 検索が完了しました',
-    resultsCount: '結果数',
-    hybridSearchFailed: '混合検索に失敗しました',
-    getContextForTopicFailed: 'トピックのコンテキスト取得に失敗しました',
-    noLLMConfigured: 'ユーザーにLLMモデルが設定されていません',
-    simpleChatGenerationError: '簡易チャット生成エラー',
-    noMatchInKnowledgeGroup: '選択された知識グループに関連する内容が見つかりませんでした。以下はモデルに基づく一般的な回答です:',
-    uploadTextSuccess: 'ノート内容を受け取りました。バックグラウンドでインデックス処理を実行中です',
-    passwordChanged: 'パスワードが正常に変更されました',
-    userCreated: 'ユーザーが正常に作成されました',
-    userInfoUpdated: 'ユーザー情報が更新されました',
-    userDeleted: 'ユーザーが削除されました',
-    pdfNoteTitle: 'PDF ノート - {date}',
-    noTextExtracted: 'テキストが抽出されませんでした',
-    kbCleared: 'ナレッジベースが空になりました',
-    fileDeleted: 'ファイルが削除されました',
-    pageImageNotFoundDetail: 'PDF の第 {page} ページの画像を取得できません',
-    groupSyncSuccess: 'ファイルグループが更新されました',
-    fileDeletedFromGroup: 'ファイルがグループから削除されました',
-    chunkConfigCorrection: 'チャンク設定の修正: {warnings}',
-    noChunksGenerated: 'ファイル {id} からテキストチャンクが生成されませんでした',
-    chunkCountAnomaly: '実際のチャンク数 {actual} が推定値 {estimated} を大幅に超えています。異常がある可能性があります',
-    batchSizeExceeded: 'バッチ {index} のサイズ {actual} が推奨値 {limit} を超えています。分割して処理します',
-    skippingEmptyVectorChunk: '空ベクトルのテキストブロック {index} をスキップします',
-    contextLengthErrorFallback: 'バッチ処理でコンテキスト長エラーが発生しました。単一テキスト処理モードにダウングレードします',
-    chunkLimitExceededForceBatch: 'チャンク数 {actual} がモデルのバッチ制限 {limit} を超えています。強制的にバッチ処理を行います',
-    noteContentRequired: 'ノート内容は必須です',
-    imageAnalysisStarted: 'モデル {id} で画像を分析中...',
-    batchAnalysisStarted: '{count} 枚の画像を分析中...',
-    pageAnalysisFailed: '第 {page} ページの分析に失敗しました',
-    visionSystemPrompt: 'あなたは専門的なドキュメント分析アシスタントです。このドキュメント画像を分析し、以下の要求に従って JSON 形式で返してください:\n\n1. すべての読み取り可能なテキストを抽出(読み取り順序に従い、段落と形式を保持)\n2. 画像/グラフ/表の識別(内容、意味、役割を記述)\n3. ページレイアウトの分析(テキストのみ/テキストと画像の混合/表/グラフなど)\n4. 分析品質の評価(0-1)\n\nレスポンス形式:\n{\n  "text": "完全なテキスト内容",\n  "images": [\n    {"type": "グラフの種類", "description": "詳細な記述", "position": 1}\n  ],\n  "layout": "レイアウトの説明",\n  "confidence": 0.95\n}',
-    visionModelCall: '[モデル呼び出し] タイプ: Vision, モデル: {model}, ページ: {page}',
-    visionAnalysisSuccess: '✅ Vision 分析完了: {path}{page}, テキスト長: {textLen}文字, 画像数: {imgCount}, レイアウト: {layout}, 信頼度: {confidence}%',
-    conversationHistoryNotFound: '会話履歴が存在しません',
-    batchContextLengthErrorFallback: '小ファイルバッチ処理でコンテキスト長エラーが発生しました。単一テキスト処理モードにダウングレードします',
-    chunkProcessingFailed: 'テキストブロック {index} の処理に失敗しました。スキップします: {message}',
-    singleTextProcessingComplete: '単一テキスト処理完了: {count} チャンク',
-    fileVectorizationComplete: 'ファイル {id} ベクトル化完了。{count} 個のテキストブロックを処理しました。最終メモリ: {memory}MB',
-    fileVectorizationFailed: 'ファイル {id} ベクトル化失敗',
-    batchProcessingStarted: 'バッチ処理を開始します: {count} アイテム',
-    batchProcessingProgress: 'バッチ {index}/{total} を処理中: {count} 個のアイテム',
-    batchProcessingComplete: 'バッチ処理完了: {count} アイテム, 所要時間 {duration}s',
-    onlyFailedFilesRetryable: '失敗したファイルのみ再試行可能です (現在のステータス: {status})',
-    emptyFileRetryFailed: 'ファイル内容が空です。再試行できません。ファイルを再アップロードしてください。',
-    ragSystemPrompt: 'あなたは専門的なナレッジベースアシスタントです。以下の提供されたドキュメントの内容に基づいて、ユーザーの質問に答えてください。',
-    ragRules: '## ルール:\n1. 提供されたドキュメントの内容のみに基づいて回答し、情報を捏造しないでください。\n2. ドキュメントに関連情報がない場合は、その旨をユーザーに伝えてください。\n3. 回答には情報源を明記してください。形式:[ファイル名.拡張子]\n4. 複数のドキュメントで情報が矛盾している場合は、総合的に分析するか、異なる視点を説明してください。\n5. {lang}で回答してください。',
-    ragDocumentContent: '## ドキュメント内容:',
-    ragUserQuestion: '## ユーザーの質問:',
-    ragAnswer: '## 回答:',
-    ragSource: '### ソース:{fileName}',
-    ragSegment: 'セグメント {index} (類似度: {score}):',
-    ragNoDocumentFound: '関連するドキュメントが見つかりませんでした。',
-    queryExpansionPrompt: 'あなたは検索アシスタントです。以下のユーザーのクエリに対して、ベクトル検索でより良い結果を得るために、3つの異なるバリエーションを生成してください。各バリエーションは異なるキーワードや表現を使用しつつ、元の意味を維持する必要があります。数字やプレフィックスなしで、3行のクエリを直接出力してください:\n\nクエリ:{query}',
-    hydePrompt: '以下のユーザーの質問に対して、簡潔で事実に基づいた仮説的な回答(約200文字)を書いてください。「私の分析によると...」などの導入文は含めず、回答内容のみを直接出力してください。\n\n質問:{query}',
+    noMatchInKnowledgeGroup: '選択された知識グループに関連する内容が見つかりませんでした',
   },
   en: {
-    searching: 'Searching knowledge base...',
-    noResults: 'No relevant knowledge found, will answer based on general knowledge...',
-    searchFailed: 'Knowledge base search failed, will answer based on general knowledge...',
-    generatingResponse: 'Generating response',
-    files: ' files',
-    notebooks: ' notebooks',
-    all: 'all',
-    items: '',
-    searchResults: 'Search results',
-    relevantInfoFound: ' relevant info found',
-    searchHits: 'Search hits',
-    relevance: 'Relevance',
-    sourceFiles: 'Source files',
-    searchScope: 'Search scope',
-    error: 'Error',
-    creatingHistory: 'Creating new chat history: ',
-    searchingModelById: 'Searching model by ID: ',
-    searchModelFallback: 'Specified embedding model not found. Using first available model.',
-    noEmbeddingModelFound: 'No embedding model settings found',
-    usingEmbeddingModel: 'Using embedding model: ',
-    startingSearch: 'Starting knowledge base search...',
-    searchResultsCount: 'Search results count: ',
-    searchFailedLog: 'Search failed',
-    chatStreamError: 'Chat stream error',
-    assistStreamError: 'Assist stream error',
-    file: 'File',
-    content: 'Content',
-    userLabel: 'User',
-    assistantLabel: 'Assistant',
-    intelligentAssistant: 'You are an intelligent writing assistant.',
-    searchString: 'Search string: ',
-    embeddingModelIdNotProvided: 'Embedding model ID not provided',
-    generatingEmbeddings: 'Generating embeddings...',
-    embeddingsGenerated: 'Embeddings generated successfully',
-    dimensions: 'dimensions',
-    performingHybridSearch: 'Performing hybrid search...',
-    esSearchCompleted: 'ES search completed',
-    resultsCount: 'Results count',
-    hybridSearchFailed: 'Hybrid search failed',
-    getContextForTopicFailed: 'getContextForTopic failed',
-    noLLMConfigured: 'No LLM model configured for user',
-    simpleChatGenerationError: 'Simple chat generation error',
-    noMatchInKnowledgeGroup: 'No relevant content found in the selected knowledge group. The following is a general answer based on the model:',
-    uploadTextSuccess: 'Note content received. Indexing in background',
-    passwordChanged: 'Password changed successfully',
-    userCreated: 'User created successfully',
-    userInfoUpdated: 'User information updated',
-    userDeleted: 'User deleted',
-    pdfNoteTitle: 'PDF Note - {date}',
-    noTextExtracted: 'No text extracted',
-    kbCleared: 'Knowledge base cleared',
-    fileDeleted: 'File deleted',
-    pageImageNotFoundDetail: 'Could not retrieve image for PDF page {page}',
-    groupSyncSuccess: 'File groups updated',
-    fileDeletedFromGroup: 'File removed from group',
-    chunkConfigCorrection: 'Chunk config corrected: {warnings}',
-    noChunksGenerated: 'No chunks generated for file {id}',
-    chunkCountAnomaly: 'Actual chunk count {actual} significantly exceeds estimate {estimated}. Possible anomaly.',
-    batchSizeExceeded: 'Batch {index} size {actual} exceeds recommended limit {limit}. Splitting for processing.',
-    skippingEmptyVectorChunk: 'Skipping text block {index} due to empty vector',
-    contextLengthErrorFallback: 'Context length error occurred during batch processing. Downgrading to single processing mode.',
-    chunkLimitExceededForceBatch: 'Chunk count {actual} exceeds model batch limit {limit}. Forcing batch processing.',
-    noteContentRequired: 'Note content is required',
-    imageAnalysisStarted: 'Analyzing image with model {id}...',
-    batchAnalysisStarted: 'Batch analyzing {count} images...',
-    pageAnalysisFailed: 'Failed to analyze page {page}',
-    visionSystemPrompt: 'You are a professional document analysis assistant. Analyze this document image and return in JSON format according to these requirements:\n\n1. Extract all readable text (follow reading order, maintain paragraphs and formatting)\n2. Identify images/graphs/tables (describe content, meaning, and role)\n3. Analyze page layout (text only/mixed/table/graph, etc.)\n4. Evaluate analysis quality (0-1)\n\nResponse format:\n{\n  "text": "full text content",\n  "images": [\n    {"type": "graph type", "description": "detailed description", "position": 1}\n  ],\n  "layout": "layout description",\n  "confidence": 0.95\n}',
-    visionModelCall: '[Model Call] Type: Vision, Model: {model}, Page: {page}',
-    visionAnalysisSuccess: '✅ Vision analysis complete: {path}{page}, Text length: {textLen}, Images: {imgCount}, Layout: {layout}, Confidence: {confidence}%',
-    conversationHistoryNotFound: 'Conversation history not found',
-    batchContextLengthErrorFallback: 'Context length error occurred during small file batch processing. Downgrading to single processing mode.',
-    chunkProcessingFailed: 'Failed to process text block {index}. Skipping: {message}',
-    singleTextProcessingComplete: 'Single text processing complete: {count} chunks',
-    fileVectorizationComplete: 'File {id} vectorization complete. Processed {count} text blocks. Final memory: {memory}MB',
-    fileVectorizationFailed: 'File {id} vectorization failed',
-    batchProcessingStarted: 'Batch processing started: {count} items',
-    batchProcessingProgress: 'Processing batch {index}/{total}: {count} items',
-    batchProcessingComplete: 'Batch processing complete: {count} items in {duration}s',
-    onlyFailedFilesRetryable: 'Only failed files can be retried (current status: {status})',
-    emptyFileRetryFailed: 'File content is empty. Cannot retry. Please re-upload the file.',
-    ragSystemPrompt: 'You are a professional knowledge base assistant. Please answer the user\'s question based on the provided document content below.',
-    ragRules: '## Rules:\n1. Answer based only on the provided document content; do not fabricate information.\n2. If there is no relevant information in the documents, please inform the user.\n3. Clearly state the sources in your answer. Format: [filename.ext]\n4. If information in different documents is contradictory, analyze it comprehensively or explain the different perspectives.\n5. Please answer in {lang}.',
-    ragDocumentContent: '## Document Content:',
-    ragUserQuestion: '## User Question:',
-    ragAnswer: '## Answer:',
-    ragSource: '### Source: {fileName}',
-    ragSegment: 'Segment {index} (Similarity: {score}):',
-    ragNoDocumentFound: 'No relevant documents found.',
-    queryExpansionPrompt: 'You are a search assistant. Please generate 3 different variations of the following user query to help get better results in vector search. Each variation should use different keywords or phrasing while maintaining the original meaning. Output the 3 queries directly as 3 lines, without numbers or prefixes:\n\nQuery: {query}',
-    hydePrompt: 'Please write a brief, factual hypothetical answer (about 100 words) to the following user question. Do not include any introductory text (like "Based on my analysis..."), just output the answer content directly.\n\nQuestion: {query}',
-  }
+    noMatchInKnowledgeGroup: 'No relevant content found in the selected knowledge group',
+  },
 };

+ 5 - 3
server/src/import-task/import-task.service.ts

@@ -6,6 +6,7 @@ import { ImportTask } from './import-task.entity';
 import { Cron, CronExpression } from '@nestjs/schedule';
 import { KnowledgeBaseService } from '../knowledge-base/knowledge-base.service';
 import { KnowledgeGroupService } from '../knowledge-group/knowledge-group.service';
+import { I18nService } from '../i18n/i18n.service';
 import * as fs from 'fs';
 import * as path from 'path';
 
@@ -19,6 +20,7 @@ export class ImportTaskService {
         private kbService: KnowledgeBaseService,
         private groupService: KnowledgeGroupService,
         private configService: ConfigService,
+        private i18nService: I18nService,
     ) { }
 
     async create(taskData: Partial<ImportTask>): Promise<ImportTask> {
@@ -45,7 +47,7 @@ export class ImportTaskService {
     async delete(taskId: string, userId: string): Promise<void> {
         const task = await this.taskRepository.findOne({ where: { id: taskId, userId } });
         if (!task) {
-            throw new Error(`Task ${taskId} not found or you don't have permission to delete it.`);
+            throw new Error(this.i18nService.getMessage('importTaskNotFound'));
         }
         await this.taskRepository.remove(task);
     }
@@ -87,7 +89,7 @@ export class ImportTaskService {
 
         try {
             if (!fs.existsSync(task.sourcePath)) {
-                throw new Error(`Directory not found: ${task.sourcePath}`);
+                throw new Error(this.i18nService.formatMessage('sourcePathNotFound', { path: task.sourcePath }));
             }
 
             const uploadPath = this.configService.get<string>('UPLOAD_FILE_PATH', './uploads');
@@ -164,7 +166,7 @@ export class ImportTaskService {
                     groupId = group.id;
                     await this.appendLog(taskId, `Created new group: ${task.targetGroupName}`);
                 } else if (!groupId) {
-                    throw new Error('No target group specified');
+                    throw new Error(this.i18nService.getMessage('targetGroupRequired'));
                 }
 
                 await this.appendLog(taskId, `Scanning directory: ${task.sourcePath}`);

+ 50 - 50
server/src/knowledge-base/chunk-config.service.ts

@@ -5,13 +5,13 @@ import { TenantService } from '../tenant/tenant.service';
 // import { UserSettingService } from '../user-setting/user-setting.service';
 
 /**
- * チャンク設定サービス
- * チャンクパラメータの検証と管理を担当し、モデルの制限や環境変数の設定に適合していることを確認します
+ * Chunk config service
+ * Responsible for validating and managing chunk parameters to ensure they conform to model limits and environment variable settings
  *
- * 制限の優先順位:
- * 1. 環境変数 (MAX_CHUNK_SIZE, MAX_OVERLAP_SIZE)
- * 2. データベース内のモデル設定 (maxInputTokens, maxBatchSize)
- * 3. デフォルト値
+ * Priority of limits:
+ * 1. Environment variables (MAX_CHUNK_SIZE, MAX_OVERLAP_SIZE)
+ * 2. Model settings in database (maxInputTokens, maxBatchSize)
+ * 3. Default values
  */
 import {
   DEFAULT_CHUNK_SIZE,
@@ -28,18 +28,18 @@ import { I18nService } from '../i18n/i18n.service';
 export class ChunkConfigService {
   private readonly logger = new Logger(ChunkConfigService.name);
 
-  // デフォルト設定
+  // Default settings
   private readonly DEFAULTS = {
     chunkSize: DEFAULT_CHUNK_SIZE,
     chunkOverlap: DEFAULT_CHUNK_OVERLAP,
     minChunkSize: MIN_CHUNK_SIZE,
     minChunkOverlap: MIN_CHUNK_OVERLAP,
-    maxOverlapRatio: DEFAULT_MAX_OVERLAP_RATIO,  // 重なりはチャンクサイズの50%まで
-    maxBatchSize: DEFAULT_MAX_BATCH_SIZE,    // デフォルトのバッチ制限
-    expectedDimensions: DEFAULT_VECTOR_DIMENSIONS, // デフォルトのベクトル次元
+    maxOverlapRatio: DEFAULT_MAX_OVERLAP_RATIO,  // Overlap up to 50% of chunk size
+    maxBatchSize: DEFAULT_MAX_BATCH_SIZE,    // Default batch limit
+    expectedDimensions: DEFAULT_VECTOR_DIMENSIONS, // Default vector dimensions
   };
 
-  // 環境変数で設定された上限(優先的に使用)
+  // Upper limits set by environment variables (used first)
   private readonly envMaxChunkSize: number;
   private readonly envMaxOverlapSize: number;
 
@@ -49,7 +49,7 @@ export class ChunkConfigService {
     private i18nService: I18nService,
     private tenantService: TenantService,
   ) {
-    // 環境変数からグローバルな上限設定を読み込む
+    // Load global limit settings from environment variables
     this.envMaxChunkSize = parseInt(
       this.configService.get<string>('MAX_CHUNK_SIZE', '8191')
     );
@@ -58,12 +58,12 @@ export class ChunkConfigService {
     );
 
     this.logger.log(
-      `環境変数設定の上限: MAX_CHUNK_SIZE=${this.envMaxChunkSize}, MAX_OVERLAP_SIZE=${this.envMaxOverlapSize}`
+      `Environment variable limits: MAX_CHUNK_SIZE=${this.envMaxChunkSize}, MAX_OVERLAP_SIZE=${this.envMaxOverlapSize}`
     );
   }
 
   /**
-   * モデルの制限設定を取得(データベースから読み込み)
+   * Get model limit settings (read from database)
    */
   async getModelLimits(modelId: string, userId: string, tenantId?: string): Promise<{
     maxInputTokens: number;
@@ -78,20 +78,20 @@ export class ChunkConfigService {
       throw new BadRequestException(this.i18nService.formatMessage('embeddingModelNotFound', { id: modelId }));
     }
 
-    // データベースのフィールドから制限を取得し、デフォルト値で補完
+    // Get limits from database fields and fill with defaults
     const maxInputTokens = modelConfig.maxInputTokens || this.envMaxChunkSize;
     const maxBatchSize = modelConfig.maxBatchSize || this.DEFAULTS.maxBatchSize;
     const expectedDimensions = modelConfig.dimensions || parseInt(this.configService.get('DEFAULT_VECTOR_DIMENSIONS', String(this.DEFAULTS.expectedDimensions)));
-    const providerName = modelConfig.providerName || '不明';
+    const providerName = modelConfig.providerName || 'unknown';
     const isVectorModel = modelConfig.isVectorModel || false;
 
     this.logger.log(
       this.i18nService.formatMessage('configLoaded', { name: modelConfig.name, id: modelConfig.modelId }) + '\n' +
-      `  - プロバイダー: ${providerName}\n` +
-      `  - Token制限: ${maxInputTokens}\n` +
-      `  - バッチ制限: ${maxBatchSize}\n` +
-      `  - ベクトル次元: ${expectedDimensions}\n` +
-      `  - ベクトルモデルか: ${isVectorModel}`,
+      `  - Provider: ${providerName}\n` +
+      `  - Token limit: ${maxInputTokens}\n` +
+      `  - Batch limit: ${maxBatchSize}\n` +
+      `  - Vector dimensions: ${expectedDimensions}\n` +
+      `  - Is vector model: ${isVectorModel}`,
     );
 
     return {
@@ -104,8 +104,8 @@ export class ChunkConfigService {
   }
 
   /**
-   * チャンク設定を検証および修正
-   * 優先順位: 環境変数の上限 > モデルの制限 > ユーザー設定
+   * Validate and fix chunk config
+   * Priority: Environment variable limits > Model limits > User settings
    */
   async validateChunkConfig(
     chunkSize: number,
@@ -123,7 +123,7 @@ export class ChunkConfigService {
     const warnings: string[] = [];
     const limits = await this.getModelLimits(modelId, userId, tenantId);
 
-    // 1. 最終的な上限を計算(環境変数とモデル制限の小さい方を選択)
+    // 1. Calculate final limits (choose smaller of env var and model limit)
     const effectiveMaxChunkSize = Math.min(
       this.envMaxChunkSize,
       limits.maxInputTokens,
@@ -134,7 +134,7 @@ export class ChunkConfigService {
       Math.floor(effectiveMaxChunkSize * this.DEFAULTS.maxOverlapRatio),
     );
 
-    // 2. チャンクサイズの上限を検証
+    // 2. Validate chunk size upper limit
     if (chunkSize > effectiveMaxChunkSize) {
       const reason =
         this.envMaxChunkSize < limits.maxInputTokens
@@ -151,7 +151,7 @@ export class ChunkConfigService {
       chunkSize = effectiveMaxChunkSize;
     }
 
-    // 3. チャンクサイズの下限を検証
+    // 3. Validate chunk size lower limit
     if (chunkSize < this.DEFAULTS.minChunkSize) {
       warnings.push(
         this.i18nService.formatMessage('chunkUnderflow', {
@@ -162,7 +162,7 @@ export class ChunkConfigService {
       chunkSize = this.DEFAULTS.minChunkSize;
     }
 
-    // 4. 重なりサイズの上限を検証(環境変数優先)
+    // 4. Validate overlap size upper limit (env var first)
     if (chunkOverlap > effectiveMaxOverlapSize) {
       warnings.push(
         this.i18nService.formatMessage('overlapOverflow', {
@@ -173,7 +173,7 @@ export class ChunkConfigService {
       chunkOverlap = effectiveMaxOverlapSize;
     }
 
-    // 5. 重なりサイズがチャンクサイズの50%を超えないことを検証
+    // 5. Validate overlap doesn't exceed 50% of chunk size
     const maxOverlapByRatio = Math.floor(
       chunkSize * this.DEFAULTS.maxOverlapRatio,
     );
@@ -197,9 +197,9 @@ export class ChunkConfigService {
       chunkOverlap = this.DEFAULTS.minChunkOverlap;
     }
 
-    // 6. バッチ処理の安全チェックを追加
-    // バッチ処理時、複数のテキストの合計長がモデルの制限を超えないようにする必要があります
-    const safetyMargin = 0.8; // 80% 安全マージン、バッチ処理のためにスペースを確保
+    // 6. Add safety check for batch processing
+    // During batch processing, ensure total length of multiple texts doesn't exceed model limits
+    const safetyMargin = 0.8; // 80% safety margin to leave space for batch processing
     const safeChunkSize = Math.floor(effectiveMaxChunkSize * safetyMargin);
 
     if (chunkSize > safeChunkSize) {
@@ -212,9 +212,9 @@ export class ChunkConfigService {
       );
     }
 
-    // 7. 推定チャンク数が妥当かチェック
+    // 7. Check if estimated chunk count is reasonable
     const estimatedChunkCount = this.estimateChunkCount(
-      1000000, // 1MB のテキストを想定
+      1000000, // Assume 1MB text
       chunkSize,
     );
 
@@ -234,7 +234,7 @@ export class ChunkConfigService {
   }
 
   /**
-   * 推奨されるバッチサイズを取得
+   * Get recommended batch size
    */
   async getRecommendedBatchSize(
     modelId: string,
@@ -244,11 +244,11 @@ export class ChunkConfigService {
   ): Promise<number> {
     const limits = await this.getModelLimits(modelId, userId, tenantId);
 
-    // 設定値とモデル制限の小さい方を選択
+    // Choose smaller of configured value and model limit
     const recommended = Math.min(
       currentBatchSize,
       limits.maxBatchSize,
-      200, // 安全のための上限
+      200, // Safety upper limit
     );
 
     if (recommended < currentBatchSize) {
@@ -261,11 +261,11 @@ export class ChunkConfigService {
       );
     }
 
-    return Math.max(10, recommended); // 最低10個
+    return Math.max(10, recommended); // Minimum 10
   }
 
   /**
-   * チャンク数を推定
+   * Estimate chunk count
    */
   estimateChunkCount(textLength: number, chunkSize: number): number {
     const chunkSizeInChars = chunkSize * 4; // 1 token ≈ 4 chars
@@ -273,7 +273,7 @@ export class ChunkConfigService {
   }
 
   /**
-   * ベクトル次元の検証
+   * Validate vector dimensions
    */
   async validateDimensions(
     modelId: string,
@@ -298,7 +298,7 @@ export class ChunkConfigService {
   }
 
   /**
-   * 設定概要を取得(ログ用)
+   * Get config summary (for logging)
    */
   async getConfigSummary(
     chunkSize: number,
@@ -310,17 +310,17 @@ export class ChunkConfigService {
     const limits = await this.getModelLimits(modelId, userId, tenantId);
 
     return [
-      `モデル: ${modelId}`,
-      `チャンクサイズ: ${chunkSize} tokens (制限: ${limits.maxInputTokens})`,
-      `重なりサイズ: ${chunkOverlap} tokens`,
-      `バッチサイズ: ${limits.maxBatchSize}`,
-      `ベクトル次元: ${limits.expectedDimensions}`,
+      `Model: ${modelId}`,
+      `Chunk size: ${chunkSize} tokens (limit: ${limits.maxInputTokens})`,
+      `Overlap size: ${chunkOverlap} tokens`,
+      `Batch size: ${limits.maxBatchSize}`,
+      `Vector dimensions: ${limits.expectedDimensions}`,
     ].join(', ');
   }
 
   /**
-   * フロントエンド用の設定制限を取得
-   * フロントエンドのスライダーの上限設定に使用
+   * Get config limits for frontend
+   * Used for frontend slider max value settings
    */
   async getFrontendLimits(
     modelId: string,
@@ -341,18 +341,18 @@ export class ChunkConfigService {
   }> {
     const limits = await this.getModelLimits(modelId, userId, tenantId);
 
-    // 最終的な上限を計算(環境変数とモデル制限の小さい方を選択)
+    // Calculate final limits (choose smaller of env var and model limit)
     const maxChunkSize = Math.min(this.envMaxChunkSize, limits.maxInputTokens);
     const maxOverlapSize = Math.min(
       this.envMaxOverlapSize,
       Math.floor(maxChunkSize * this.DEFAULTS.maxOverlapRatio),
     );
 
-    // モデル設定名を取得
+    // Get model config name
     const modelConfig = await this.modelConfigService.findOne(modelId, userId, tenantId || '');
     const modelName = modelConfig?.name || 'Unknown';
 
-    // テナントまたはユーザー設定からデフォルト値を取得
+    // Get defaults from tenant or user settings
     let defaultChunkSize = this.DEFAULTS.chunkSize;
     let defaultOverlapSize = this.DEFAULTS.chunkOverlap;
 

+ 43 - 41
server/src/knowledge-base/embedding.service.ts

@@ -1,6 +1,7 @@
 import { Injectable, Logger } from '@nestjs/common';
 import { ConfigService } from '@nestjs/config';
 import { ModelConfigService } from '../model-config/model-config.service';
+import { I18nService } from '../i18n/i18n.service';
 
 export interface EmbeddingResponse {
   data: Array<{
@@ -22,11 +23,12 @@ export class EmbeddingService {
   constructor(
     private modelConfigService: ModelConfigService,
     private configService: ConfigService,
+    private i18nService: I18nService,
   ) {
     this.defaultDimensions = parseInt(
       this.configService.get<string>('DEFAULT_VECTOR_DIMENSIONS', '2560'),
     );
-    this.logger.log(`デフォルトのベクトル次元が ${this.defaultDimensions} に設定されました`);
+    this.logger.log(`Default vector dimensions set to ${this.defaultDimensions}`);
   }
 
   async getEmbeddings(
@@ -35,7 +37,7 @@ export class EmbeddingService {
     embeddingModelConfigId: string,
     tenantId?: string,
   ): Promise<number[][]> {
-    this.logger.log(`${texts.length} 個のテキストに対して埋め込みベクトルを生成しています`);
+    this.logger.log(`Generating embeddings for ${texts.length} texts`);
 
     const modelConfig = await this.modelConfigService.findOne(
       embeddingModelConfigId,
@@ -43,26 +45,26 @@ export class EmbeddingService {
       tenantId || 'default',
     );
     if (!modelConfig || modelConfig.type !== 'embedding') {
-      throw new Error(`埋め込みモデル設定 ${embeddingModelConfigId} が見つかりません`);
+      throw new Error(this.i18nService.formatMessage('embeddingModelNotFound', { id: embeddingModelConfigId }));
     }
 
     if (modelConfig.isEnabled === false) {
-      throw new Error(`モデル ${modelConfig.name} は無効化されているため、埋め込みベクトルを生成できません`);
+      throw new Error(`Model ${modelConfig.name} is disabled and cannot generate embeddings`);
     }
 
-    // APIキーはオプションです - ローカルモデルを許可します
+    // API key is optional - allows local models
 
     if (!modelConfig.baseUrl) {
-      throw new Error(`モデル ${modelConfig.name} に baseUrl が設定されていません`);
+      throw new Error(`Model ${modelConfig.name} does not have baseUrl configured`);
     }
 
-    // モデル名に基づいて最大バッチサイズを決定
+    // Determine max batch size based on model name
     const maxBatchSize = this.getMaxBatchSizeForModel(modelConfig.modelId, modelConfig.maxBatchSize);
 
-    // バッチサイズが制限を超える場合は分割して処理
+    // Split processing if batch size exceeds limit
     if (texts.length > maxBatchSize) {
       this.logger.log(
-        `テキスト数 ${texts.length} がモデルのバッチ制限 ${maxBatchSize} を超えているため、分割処理します`
+        `Splitting ${texts.length} texts into batches (model batch limit: ${maxBatchSize})`
       );
 
       const allEmbeddings: number[][] = [];
@@ -78,15 +80,15 @@ export class EmbeddingService {
 
         allEmbeddings.push(...batchEmbeddings);
 
-        // APIレート制限対策のため、短い間隔で待機
+        // Wait briefly to avoid API rate limiting
         if (i + maxBatchSize < texts.length) {
-          await new Promise(resolve => setTimeout(resolve, 100)); // 100ms待機
+          await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms
         }
       }
 
       return allEmbeddings;
     } else {
-      // 通常処理(バッチサイズ以内)
+      // Normal processing (within batch size)
       return await this.getEmbeddingsForBatch(
         texts,
         userId,
@@ -97,23 +99,23 @@ export class EmbeddingService {
   }
 
   /**
-   * モデルIDに基づいて最大バッチサイズを決定
+   * Determine max batch size based on model ID
    */
   private getMaxBatchSizeForModel(modelId: string, configuredMaxBatchSize?: number): number {
-    // モデル固有のバッチサイズ制限
+    // Model-specific batch size limits
     if (modelId.includes('text-embedding-004') || modelId.includes('text-embedding-v4') ||
       modelId.includes('text-embedding-ada-002')) {
-      return Math.min(10, configuredMaxBatchSize || 100); // Googleの場合は10を上限
+      return Math.min(10, configuredMaxBatchSize || 100); // Google limit: 10
     } else if (modelId.includes('text-embedding-3') || modelId.includes('text-embedding-003')) {
-      return Math.min(2048, configuredMaxBatchSize || 2048); // OpenAI v3は2048が上限
+      return Math.min(2048, configuredMaxBatchSize || 2048); // OpenAI v3 limit: 2048
     } else {
-      // デフォルトでは設定された最大バッチサイズか100の小さい方
+      // Default: smaller of configured max or 100
       return Math.min(configuredMaxBatchSize || 100, 100);
     }
   }
 
   /**
-   * 単一バッチの埋め込み処理
+   * Process single batch embedding
    */
   private async getEmbeddingsForBatch(
     texts: string[],
@@ -136,8 +138,8 @@ export class EmbeddingService {
           this.logger.error(`Embedding API timeout after 60s: ${apiUrl}`);
         }, 60000); // 60s timeout
 
-        this.logger.log(`[モデル呼び出し] タイプ: Embedding, モデル: ${modelConfig.name} (${modelConfig.modelId}), ユーザー: ${userId}, テキスト数: ${texts.length}`);
-        this.logger.log(`埋め込み API を呼び出し中 (試行 ${attempt}/${MAX_RETRIES}): ${apiUrl}`);
+        this.logger.log(`[Model call] Type: Embedding, Model: ${modelConfig.name} (${modelConfig.modelId}), User: ${userId}, Text count: ${texts.length}`);
+        this.logger.log(`Calling embedding API (attempt ${attempt}/${MAX_RETRIES}): ${apiUrl}`);
 
         let response;
         try {
@@ -161,14 +163,14 @@ export class EmbeddingService {
         if (!response.ok) {
           const errorText = await response.text();
 
-          // バッチサイズ制限エラーを検出
+          // Detect batch size limit error
           if (errorText.includes('batch size is invalid') || errorText.includes('batch_size') ||
             errorText.includes('invalid') || errorText.includes('larger than')) {
             this.logger.warn(
-              `バッチサイズ制限エラーが検出されました。バッチサイズを半分に分割して再試行します: ${maxBatchSize} -> ${Math.floor(maxBatchSize / 2)}`
+              `Batch size limit error detected. Splitting batch in half and retrying: ${maxBatchSize} -> ${Math.floor(maxBatchSize / 2)}`
             );
 
-            // バッチをさらに小さな単位に分割して再試行
+            // Split batch into smaller units and retry
             if (texts.length > 1) {
               const midPoint = Math.floor(texts.length / 2);
               const firstHalf = texts.slice(0, midPoint);
@@ -181,51 +183,51 @@ export class EmbeddingService {
             }
           }
 
-          // コンテキスト長の過剰エラーを検出
+          // Detect context length excess error
           if (errorText.includes('context length') || errorText.includes('exceeds')) {
             const avgLength = texts.reduce((s, t) => s + t.length, 0) / texts.length;
             const totalLength = texts.reduce((s, t) => s + t.length, 0);
             this.logger.error(
-              `テキスト長が制限を超過しました: 入力 ${texts.length} 個のテキスト、` +
-              `総計 ${totalLength} 文字、平均 ${Math.round(avgLength)} 文字、` +
-              `モデル制限: ${modelConfig.maxInputTokens || 8192} tokens`
+              `Text length exceeds limit: ${texts.length} texts, ` +
+              `total ${totalLength} characters, average ${Math.round(avgLength)} characters, ` +
+              `model limit: ${modelConfig.maxInputTokens || 8192} tokens`
             );
             throw new Error(
-              `テキスト長がモデルの制限を超えています。` +
-              `現在: ${texts.length} 個のテキストで計 ${totalLength} 文字、` +
-              `モデル制限: ${modelConfig.maxInputTokens || 8192} tokens。` +
-              `アドバイス: チャンクサイズまたはバッチサイズを小さくしてください`
+              `Text length exceeds model limit. ` +
+              `Current: ${texts.length} texts with total ${totalLength} characters, ` +
+              `model limit: ${modelConfig.maxInputTokens || 8192} tokens. ` +
+              `Advice: Reduce chunk size or batch size`
             );
           }
 
-          // 429 (Too Many Requests) または 5xx (Server Error) の場合は再試行
+          // Retry on 429 (Too Many Requests) or 5xx (Server Error)
           if (response.status === 429 || response.status >= 500) {
-            this.logger.warn(`埋め込み API で一時的なエラーが発生しました (${response.status}): ${errorText}`);
+            this.logger.warn(`Temporary error from embedding API (${response.status}): ${errorText}`);
             throw new Error(`API Error ${response.status}: ${errorText}`);
           }
 
-          this.logger.error(`埋め込み API エラーの詳細: ${errorText}`);
-          this.logger.error(`リクエストパラメータ: model=${modelConfig.modelId}, inputLength=${texts[0]?.length}`);
-          throw new Error(`埋め込み API の呼び出しに失敗しました: ${response.statusText} - ${errorText}`);
+          this.logger.error(`Embedding API error details: ${errorText}`);
+          this.logger.error(`Request parameters: model=${modelConfig.modelId}, inputLength=${texts[0]?.length}`);
+          throw new Error(`Embedding API call failed: ${response.statusText} - ${errorText}`);
         }
 
         const data: EmbeddingResponse = await response.json();
         const embeddings = data.data.map((item) => item.embedding);
 
-        // 実際のレスポンスから次元を取得
+        // Get dimensions from actual response
         const actualDimensions = embeddings[0]?.length || this.defaultDimensions;
         this.logger.log(
-          `${modelConfig.name} から ${embeddings.length} 個の埋め込みベクトルを取得しました。次元: ${actualDimensions}`,
+          `Got ${embeddings.length} embedding vectors from ${modelConfig.name}. Dimensions: ${actualDimensions}`,
         );
 
         return embeddings;
       } catch (error) {
         lastError = error;
 
-        // 最後のアテンプトでなく、エラーが一時的と思われる場合(または堅牢性のために全て)は、待機後に再試行
+        // If not the last attempt and error appears temporary (or for robustness on all), retry after waiting
         if (attempt < MAX_RETRIES) {
           const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s
-          this.logger.warn(`埋め込みリクエストが失敗しました。${delay}ms 後に再試行します: ${error.message}`);
+          this.logger.warn(`Embedding request failed. Retrying after ${delay}ms: ${error.message}`);
           await new Promise(resolve => setTimeout(resolve, delay));
           continue;
         }
@@ -236,7 +238,7 @@ export class EmbeddingService {
   }
 
   private getEstimatedDimensions(modelId: string): number {
-    // 使用环境变量的默认维度
+    // Use default dimensions from environment variable
     return this.defaultDimensions;
   }
 }

+ 12 - 12
server/src/knowledge-base/knowledge-base.controller.ts

@@ -103,8 +103,8 @@ export class KnowledgeBaseController {
 
 
   /**
-   * チャンク設定の制限を取得(フロントエンドのスライダー設定用)
-   * クエリパラメータ: embeddingModelId - 埋め込みモデルID
+   * Get chunk config limits (for frontend slider settings)
+   * Query parameter: embeddingModelId - embedding model ID
    */
   @Get('chunk-config/limits')
   async getChunkConfigLimits(
@@ -134,7 +134,7 @@ export class KnowledgeBaseController {
     );
   }
 
-  // 文件分组管理 - 需要管理员权限
+  // File group management - requires admin permission
   @Post(':id/groups')
   @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
   async addFileToGroups(
@@ -167,7 +167,7 @@ export class KnowledgeBaseController {
     return { message: this.i18nService.getMessage('fileDeletedFromGroup') };
   }
 
-  // PDF プレビュー - 公開アクセス
+  // PDF preview - public access
   @Public()
   @Get(':id/pdf')
   async getPDFPreview(
@@ -183,7 +183,7 @@ export class KnowledgeBaseController {
       const jwt = await import('jsonwebtoken');
       const secret = process.env.JWT_SECRET;
       if (!secret) {
-        throw new InternalServerErrorException('JWT_SECRET environment variable is required but not set');
+        throw new InternalServerErrorException(this.i18nService.getMessage('jwtSecretRequired'));
       }
 
       let decoded;
@@ -216,7 +216,7 @@ export class KnowledgeBaseController {
       if (stat.size === 0) {
         this.logger.warn(`PDF file is empty: ${pdfPath}`);
         try {
-          fs.unlinkSync(pdfPath); // 空のファイルを削除
+          fs.unlinkSync(pdfPath); // Delete empty file
         } catch (e) { }
         throw new NotFoundException(this.i18nService.getMessage('pdfFileEmpty'));
       }
@@ -235,7 +235,7 @@ export class KnowledgeBaseController {
     }
   }
 
-  // PDF プレビューアドレスを取得
+  // Get PDF preview URL
   @Get(':id/pdf-url')
   async getPDFUrl(
     @Param('id') fileId: string,
@@ -243,15 +243,15 @@ export class KnowledgeBaseController {
     @Request() req,
   ) {
     try {
-      // PDF 変換をトリガー
+      // Trigger PDF conversion
       await this.knowledgeBaseService.ensurePDFExists(fileId, req.user.id, req.user.tenantId, force === 'true');
 
-      // 一時的なアクセストークンを生成
+      // Generate temporary access token
       const jwt = await import('jsonwebtoken');
 
       const secret = process.env.JWT_SECRET;
       if (!secret) {
-        throw new InternalServerErrorException('JWT_SECRET environment variable is required but not set');
+        throw new InternalServerErrorException(this.i18nService.getMessage('jwtSecretRequired'));
       }
 
       const token = jwt.sign(
@@ -279,7 +279,7 @@ export class KnowledgeBaseController {
     return await this.knowledgeBaseService.getPDFStatus(fileId, req.user.id, req.user.tenantId);
   }
 
-  // PDF の特定ページの画像を取得
+  // Get specific page of PDF as image
   @Get(':id/page/:index')
   async getPageImage(
     @Param('id') fileId: string,
@@ -302,7 +302,7 @@ export class KnowledgeBaseController {
 
       res.sendFile(path.resolve(imagePath));
     } catch (error) {
-      this.logger.error(`PDF ページの画像取得に失敗しました: ${error.message}`);
+      this.logger.error(`Failed to get PDF page image: ${error.message}`);
       throw new NotFoundException(this.i18nService.getMessage('pdfPageImageFailed'));
     }
   }

+ 9 - 9
server/src/knowledge-base/knowledge-base.entity.ts

@@ -14,14 +14,14 @@ import { Tenant } from '../tenant/tenant.entity';
 export enum FileStatus {
   PENDING = 'pending',
   INDEXING = 'indexing',
-  EXTRACTED = 'extracted', // テキスト抽出が完了し、データベースに保存されました
-  VECTORIZED = 'vectorized', // ベクトル化が完了し、ES にインデックスされました
+  EXTRACTED = 'extracted', // Text extraction completed and saved to database
+  VECTORIZED = 'vectorized', // Vectorization completed and indexed to ES
   FAILED = 'failed',
 }
 
 export enum ProcessingMode {
-  FAST = 'fast',      // 高速モード - Tika を使用
-  PRECISE = 'precise', // 精密モード - Vision Pipeline を使用
+  FAST = 'fast',      // Fast mode - use Tika
+  PRECISE = 'precise', // Precise mode - use Vision Pipeline
 }
 
 @Entity('knowledge_bases')
@@ -51,7 +51,7 @@ export class KnowledgeBase {
   })
   status: FileStatus;
 
-  @Column({ name: 'user_id', nullable: true }) // 暫定的に空を許可(デバッグ用)、将来的には必須にすべき
+  @Column({ name: 'user_id', nullable: true }) // Temporarily allowed empty (for debugging), should be required in future
   userId: string;
 
   @Column({ name: 'tenant_id', nullable: true, type: 'text' })
@@ -62,9 +62,9 @@ export class KnowledgeBase {
   tenant: Tenant;
 
   @Column({ type: 'text', nullable: true })
-  content: string; // Tika で抽出されたテキスト内容を保存
+  content: string; // Stores text content extracted by Tika
 
-  // インデックス設定パラメータ
+  // Index setting parameters
   @Column({ name: 'chunk_size', type: 'integer', default: 1000 })
   chunkSize: number;
 
@@ -83,10 +83,10 @@ export class KnowledgeBase {
   processingMode: ProcessingMode;
 
   @Column({ type: 'json', nullable: true })
-  metadata: any; // 追加のメタデータを保存(画像の説明、信頼度など)
+  metadata: any; // Stores additional metadata (image descriptions, confidence, etc.)
 
   @Column({ name: 'pdf_path', nullable: true })
-  pdfPath: string; // PDF ファイルパス(プレビュー用)
+  pdfPath: string; // PDF file path (for preview)
 
   @ManyToMany(() => KnowledgeGroup, (group) => group.knowledgeBases)
   groups: KnowledgeGroup[];

+ 139 - 139
server/src/knowledge-base/knowledge-base.service.ts

@@ -78,7 +78,7 @@ export class KnowledgeBaseService {
       processingMode: processingMode,
     });
 
-    // 分類(グループ)の関連付け
+    // Associate groups
     if (config?.groupIds && config.groupIds.length > 0) {
       const groups = await this.groupRepository.find({
         where: { id: In(config.groupIds), tenantId: tenantId }
@@ -141,14 +141,14 @@ export class KnowledgeBaseService {
     }
     return this.kbRepository.find({
       where,
-      relations: ['groups'], // グループリレーションをロード
+      relations: ['groups'], // Load group relations
       order: { createdAt: 'DESC' },
     });
   }
 
   async searchKnowledge(userId: string, tenantId: string, query: string, topK: number = 5) {
     try {
-      // 環境変数のデフォルト次元数を使用してシミュレーションベクトルを生成
+      // Generate simulation vector using default dimensions from environment variable
       const defaultDimensions = parseInt(
         process.env.DEFAULT_VECTOR_DIMENSIONS || '2560',
       );
@@ -246,11 +246,11 @@ export class KnowledgeBaseService {
         `RAG search failed for user ${userId}:`,
         error.stack || error.message,
       );
-      // エラーをスローするのではなく空の結果を返し、システムの稼働を継続させる
+      // Return empty result instead of throwing error to keep system running
       return {
         searchResults: [],
         sources: [],
-        ragPrompt: query, // オリジナルのクエリを使用
+        ragPrompt: query, // Use original query
         hasRelevantContent: false,
       };
     }
@@ -345,18 +345,18 @@ export class KnowledgeBaseService {
         return;
       }
 
-      // メモリ監視 - 処理前チェック
+      // Memory monitor - pre-processing check
       const memBefore = this.memoryMonitor.getMemoryUsage();
-      this.logger.log(`メモリ状態 - 処理前: ${memBefore.heapUsed}/${memBefore.heapTotal}MB`);
+      this.logger.log(`Memory state - before processing: ${memBefore.heapUsed}/${memBefore.heapTotal}MB`);
 
-      // モードに基づいて処理フローを選択
+      // Select processing flow based on mode
       const mode = config?.mode || 'fast';
 
       if (mode === 'precise') {
-        // 精密モード - Vision Pipeline を使用
+        // Precise mode - use Vision Pipeline
         await this.processPreciseMode(kb, userId, tenantId, config);
       } else {
-        // 高速モード - Tika を使用
+        // Fast mode - use Tika
         await this.processFastMode(kb, userId, tenantId, config);
       }
 
@@ -368,13 +368,13 @@ export class KnowledgeBaseService {
   }
 
   /**
-   * 高速モード処理(既存フロー)
+   * Fast mode processing (existing flow)
    */
   private async processFastMode(kb: KnowledgeBase, userId: string, tenantId: string, config?: any) {
-    // 1. Tika を使用してテキストを抽出
+    // 1. Extract text using Tika
     let text = await this.tikaService.extractText(kb.storagePath);
 
-    // 画像ファイルの場合はビジョンモデルを使用
+    // Use vision model for image files
     if (this.visionService.isImageFile(kb.mimetype)) {
       const settings = await this.tenantService.getSettings(tenantId || 'default');
       const visionModelId = settings?.selectedVisionId;
@@ -398,37 +398,37 @@ export class KnowledgeBaseService {
       this.logger.warn(this.i18nService.getMessage('noTextExtracted'));
     }
 
-    // テキストサイズを確認
+    // Check text size
     const textSizeMB = Math.round(text.length / 1024 / 1024);
     if (textSizeMB > 50) {
       this.logger.warn(this.i18nService.formatMessage('extractedTextTooLarge', { size: textSizeMB }));
     }
 
-    // テキストをデータベースに保存
+    // Save text to database
     await this.kbRepository.update(kb.id, { content: text });
     await this.updateStatus(kb.id, FileStatus.EXTRACTED);
 
-    // 非同期ベクトル化
+    // Async vectorization
     await this.vectorizeToElasticsearch(kb.id, userId, tenantId, text, config).catch((err) => {
       this.logger.error(`Error vectorizing file ${kb.id}`, err);
     });
 
-    // 自動タイトル生成 (非同期的に実行)
+    // Auto-generate title (async execution)
     this.generateTitle(kb.id).catch((err) => {
       this.logger.error(`Error generating title for file ${kb.id}`, err);
     });
 
-    // 非同期的に PDF 変換をトリガー(ドキュメントファイルの場合)
+    // Trigger PDF conversion asynchronously (for document files)
     this.ensurePDFExists(kb.id, userId, tenantId).catch((err) => {
       this.logger.warn(this.i18nService.formatMessage('pdfConversionFailedDetail', { id: kb.id }), err);
     });
   }
 
   /**
-   * 精密モード処理(新規フロー)
+   * Precise mode processing (new flow)
    */
   private async processPreciseMode(kb: KnowledgeBase, userId: string, tenantId: string, config?: any) {
-    // 精密モードがサポートされているか確認
+    // Check if precise mode is supported
     const preciseFormats = ['.pdf', '.doc', '.docx', '.ppt', '.pptx'];
     const ext = kb.originalName.toLowerCase().substring(kb.originalName.lastIndexOf('.'));
 
@@ -439,7 +439,7 @@ export class KnowledgeBaseService {
       return this.processFastMode(kb, userId, tenantId, config);
     }
 
-    // Vision モデルが設定されているか確認
+    // Check if Vision model is configured
       const settings = await this.tenantService.getSettings(tenantId || 'default');
     const visionModelId = settings?.selectedVisionId;
     if (!visionModelId) {
@@ -461,7 +461,7 @@ export class KnowledgeBaseService {
       return this.processFastMode(kb, userId, tenantId, config);
     }
 
-    // Vision Pipeline を呼び出し
+    // Call Vision Pipeline
     try {
       const result = await this.visionPipelineService.processPreciseMode(
         kb.storagePath,
@@ -481,7 +481,7 @@ export class KnowledgeBaseService {
         return this.processFastMode(kb, userId, tenantId, config);
       }
 
-      // テキスト内容をデータベースに保存
+      // Save text content to database
       const combinedText = result.results.map(r => r.text).join('\n\n');
       const metadata = {
         processedPages: result.processedPages,
@@ -505,18 +505,18 @@ export class KnowledgeBaseService {
         this.i18nService.formatMessage('preciseModeComplete', { pages: result.processedPages, cost: result.cost.toFixed(2) })
       );
 
-      // 非同期でベクトル化し、Elasticsearch にインデックス
-      // 各ページを独立したドキュメントとして作成し、メタデータを保持
+      // Async vectorization and Elasticsearch indexing
+      // Create each page as separate document with metadata
       this.indexPreciseResults(kb, userId, tenantId, kb.embeddingModelId, result.results).catch((err) => {
         this.logger.error(`Error indexing precise results for ${kb.id}`, err);
       });
 
-      // 非同期で PDF 変換をトリガー
+      // Trigger PDF conversion asynchronously
       this.ensurePDFExists(kb.id, userId, tenantId).catch((err) => {
         this.logger.warn(`Initial PDF conversion failed for ${kb.id}`, err);
       });
 
-      // 自動タイトル生成 (非同期的に実行)
+      // Auto-generate title (async execution)
       this.generateTitle(kb.id).catch((err) => {
         this.logger.error(`Error generating title for file ${kb.id}`, err);
       });
@@ -528,7 +528,7 @@ export class KnowledgeBaseService {
   }
 
   /**
-   * 精密モードの結果をインデックス
+   * Index precise mode results
    */
   private async indexPreciseResults(
     kb: KnowledgeBase,
@@ -539,11 +539,11 @@ export class KnowledgeBaseService {
   ): Promise<void> {
     this.logger.log(`Indexing ${results.length} precise results for ${kb.id}`);
 
-    // インデックスの存在を確認 - 実際のモデル次元数を取得
+    // Check index existence - get actual model dimensions
     const actualDimensions = await this.getActualModelDimensions(embeddingModelId, userId, tenantId);
     await this.elasticsearchService.createIndexIfNotExists(actualDimensions);
 
-    // ベクトル化とインデックスをバッチ処理
+    // Batch vectorization and indexing
     const batchSize = parseInt(process.env.CHUNK_BATCH_SIZE || '50');
 
     for (let i = 0; i < results.length; i += batchSize) {
@@ -551,14 +551,14 @@ export class KnowledgeBaseService {
       const texts = batch.map(r => r.text);
 
       try {
-        // ベクトルを生成
+        // Generate vectors
         const embeddings = await this.embeddingService.getEmbeddings(
           texts,
           userId,
           embeddingModelId
         );
 
-        // 各結果をインデックス
+        // Index each result
         for (let j = 0; j < batch.length; j++) {
           const result = batch[j];
           const embedding = embeddings[j];
@@ -588,30 +588,30 @@ export class KnowledgeBaseService {
           );
         }
 
-        this.logger.log(`バッチ ${Math.floor(i / batchSize) + 1} 完了: ${batch.length} ページ`);
+        this.logger.log(`Batch ${Math.floor(i / batchSize) + 1} completed: ${batch.length} pages`);
       } catch (error) {
-        this.logger.error(`バッチ ${Math.floor(i / batchSize) + 1} の処理に失敗しました`, error);
+        this.logger.error(`Batch ${Math.floor(i / batchSize) + 1} processing failed`, error);
       }
     }
 
     await this.updateStatus(kb.id, FileStatus.VECTORIZED);
-    this.logger.log(`精密モードのインデックス完了: ${results.length} ページ`);
+    this.logger.log(`Precise mode indexing completed: ${results.length} pages`);
   }
 
   /**
-   * PDF の特定ページの画像を取得
+   * Get specific page of PDF as image
    */
   async getPageAsImage(fileId: string, pageIndex: number, userId: string, tenantId: string): Promise<string> {
     const pdfPath = await this.ensurePDFExists(fileId, userId, tenantId);
 
-    // 特定のページを変換
+    // Convert specific pages
     const result = await this.pdf2ImageService.convertToImages(pdfPath, {
       density: 150,
       quality: 75,
       format: 'jpeg',
     });
 
-    // 対応するページ番号の画像を見つける
+    // Find images for corresponding page numbers
     const pageImage = result.images.find(img => img.pageIndex === pageIndex + 1);
     if (!pageImage) {
       throw new NotFoundException(this.i18nService.formatMessage('pageImageNotFoundDetail', { page: pageIndex + 1 }));
@@ -631,14 +631,14 @@ export class KnowledgeBaseService {
       const kb = await this.kbRepository.findOne({ where: { id: kbId, tenantId } });
       if (!kb) return;
 
-      // メモリ監視 - ベクトル化前チェック
+      // Memory monitor - pre-vectorization check
       const memBeforeChunk = this.memoryMonitor.getMemoryUsage();
       this.logger.log(
-        `ベクトル化前メモリ: ${memBeforeChunk.heapUsed}/${memBeforeChunk.heapTotal}MB`,
+        `Pre-vectorization memory: ${memBeforeChunk.heapUsed}/${memBeforeChunk.heapTotal}MB`,
       );
 
       this.logger.debug(`File ${kbId}: Validating chunk config...`);
-      // 1. チャンク設定の検証と修正(モデルの制限と環境変数に基づく)
+      // 1. Validate and fix chunk config (based on model limits and env vars)
       const validatedConfig = await this.chunkConfigService.validateChunkConfig(
         kb.chunkSize,
         kb.chunkOverlap,
@@ -647,13 +647,13 @@ export class KnowledgeBaseService {
       );
       this.logger.debug(`File ${kbId}: Chunk config validated.`);
 
-      // 設定が修正された場合、警告を記録しデータベースを更新
+      // If config modified, log warning and update database
       if (validatedConfig.warnings.length > 0) {
         this.logger.warn(
           this.i18nService.formatMessage('chunkConfigCorrection', { warnings: validatedConfig.warnings.join(', ') })
         );
 
-        // データベース内の設定を更新
+        // Update config in database
         if (validatedConfig.chunkSize !== kb.chunkSize ||
           validatedConfig.chunkOverlap !== kb.chunkOverlap) {
           await this.kbRepository.update(kbId, {
@@ -663,7 +663,7 @@ export class KnowledgeBaseService {
         }
       }
 
-      // 設定サマリーを表示(実際に適用される上限を含む)
+      // Display config summary (including actual limits applied)
       this.logger.debug(`File ${kbId}: Getting config summary...`);
       const configSummary = await this.chunkConfigService.getConfigSummary(
         validatedConfig.chunkSize,
@@ -671,16 +671,16 @@ export class KnowledgeBaseService {
         kb.embeddingModelId,
         userId,
       );
-      this.logger.log(`チャンク設定: ${configSummary}`);
-      this.logger.log(`設定上限: チャンク=${validatedConfig.effectiveMaxChunkSize}, 重複=${validatedConfig.effectiveMaxOverlapSize}`);
+      this.logger.log(`Chunk config: ${configSummary}`);
+      this.logger.log(`Config limits: chunk=${validatedConfig.effectiveMaxChunkSize}, overlap=${validatedConfig.effectiveMaxOverlapSize}`);
 
-      // 2. 検証済みの設定を使用してチャンク分割
+      // 2. Split text using validated config
       const chunks = this.textChunkerService.chunkText(
         text,
         validatedConfig.chunkSize,
         validatedConfig.chunkOverlap,
       );
-      this.logger.log(`ファイル ${kbId} から ${chunks.length} 個のテキストブロックを分割しました`);
+      this.logger.log(`File ${kbId} split into ${chunks.length} text blocks`);
 
       if (chunks.length === 0) {
         this.logger.warn(this.i18nService.formatMessage('noChunksGenerated', { id: kbId }));
@@ -688,7 +688,7 @@ export class KnowledgeBaseService {
         return;
       }
 
-      // 3. チャンク数が妥当か確認
+      // 3. Validate chunk count is reasonable
       const estimatedChunkCount = this.chunkConfigService.estimateChunkCount(
         text.length,
         validatedConfig.chunkSize,
@@ -699,7 +699,7 @@ export class KnowledgeBaseService {
         );
       }
 
-      // 4. 推奨バッチサイズを取得(モデルの制限に基づく)
+      // 4. Get recommended batch size (based on model limits)
       const recommendedBatchSize = await this.chunkConfigService.getRecommendedBatchSize(
         kb.embeddingModelId,
         userId,
@@ -707,20 +707,20 @@ export class KnowledgeBaseService {
         parseInt(process.env.CHUNK_BATCH_SIZE || '100'),
       );
 
-      // 5. メモリ使用量を推定
+      // 5. Estimate memory usage
       const avgChunkSize = chunks.reduce((sum, c) => sum + c.content.length, 0) / chunks.length;
       const estimatedMemory = this.memoryMonitor.estimateMemoryUsage(
         chunks.length,
         avgChunkSize,
         parseInt(process.env.DEFAULT_VECTOR_DIMENSIONS || '2560'),
       );
-      this.logger.log(`推定メモリ使用量: ${estimatedMemory}MB (バッチサイズ: ${recommendedBatchSize})`);
+      this.logger.log(`Estimated memory usage: ${estimatedMemory}MB (batch size: ${recommendedBatchSize})`);
 
-      // 6. 実際のモデル次元数を取得し、インデックスの存在を確認
+      // 6. Get actual model dimensions and check index exists
       const actualDimensions = await this.getActualModelDimensions(kb.embeddingModelId, userId, tenantId);
       await this.elasticsearchService.createIndexIfNotExists(actualDimensions);
 
-      // 7. ベクトル化とインデックス作成をバッチ処理
+      // 7. Batch vectorization and indexing
       const useBatching = this.memoryMonitor.shouldUseBatching(
         chunks.length,
         avgChunkSize,
@@ -732,7 +732,7 @@ export class KnowledgeBaseService {
           await this.processInBatches(
             chunks,
             async (batch, batchIndex) => {
-              // バッチサイズがモデルの制限を超えていないか検証
+              // Verify batch size not exceeding model limit
               if (batch.length > recommendedBatchSize) {
                 this.logger.warn(
                   this.i18nService.formatMessage('batchSizeExceeded', { index: batchIndex, actual: batch.length, limit: recommendedBatchSize })
@@ -746,14 +746,14 @@ export class KnowledgeBaseService {
                 kb.embeddingModelId,
               );
 
-              // 次元の整合性を検証
+              // Validate dimension consistency
               if (embeddings.length > 0 && embeddings[0].length !== actualDimensions) {
                 this.logger.warn(
-                  `ベクトル次元が不一致です: 期待値 ${actualDimensions}, 実際 ${embeddings[0].length}`
+                  `Vector dimension mismatch: expected ${actualDimensions}, got ${embeddings[0].length}`
                 );
               }
 
-              // このバッチデータを即座にインデックス
+              // Index this batch data immediately
               for (let i = 0; i < batch.length; i++) {
                 const chunk = batch[i];
                 const embedding = embeddings[i];
@@ -779,30 +779,30 @@ export class KnowledgeBaseService {
                 );
               }
 
-              this.logger.log(`バッチ ${batchIndex} 完了: ${batch.length} チャンク`);
+              this.logger.log(`Batch ${batchIndex} completed: ${batch.length} chunks`);
             },
             {
               batchSize: recommendedBatchSize,
               onBatchComplete: (batchIndex, totalBatches) => {
                 const mem = this.memoryMonitor.getMemoryUsage();
                 this.logger.log(
-                  `バッチ ${batchIndex}/${totalBatches} 完了, メモリ: ${mem.heapUsed}MB`,
+                  `Batch ${batchIndex}/${totalBatches} completed, memory: ${mem.heapUsed}MB`,
                 );
               },
             },
           );
         } catch (error) {
-          // コンテキスト長エラーを検出(日本語・中国語・英語に対応)
-          if (error.message && (error.message.includes('context length') || error.message.includes('コンテキスト長が上限を超えています') || error.message.includes('コンテキスト長が上限を超えています'))) {
+          // Detect context length error (supports Japanese/Chinese/English)
+          if (error.message && (error.message.includes('context length') || error.message.includes('context length exceeded'))) {
             this.logger.warn(this.i18nService.getMessage('contextLengthErrorFallback'));
 
-            // 単一テキスト処理にダウングレード
+            // Downgrade to single text processing
             for (let i = 0; i < chunks.length; i++) {
               const chunk = chunks[i];
 
               try {
                 const embeddings = await this.embeddingService.getEmbeddings(
-                  [chunk.content], // 単一テキスト
+                  [chunk.content], // Single text
                   userId,
                   kb.embeddingModelId,
                 );
@@ -829,27 +829,27 @@ export class KnowledgeBaseService {
                 );
 
                 if ((i + 1) % 10 === 0) {
-                  this.logger.log(`単一処理進捗: ${i + 1}/${chunks.length}`);
+                  this.logger.log(`Single processing progress: ${i + 1}/${chunks.length}`);
                 }
               } catch (chunkError) {
                 this.logger.error(
-                  `テキストブロック ${chunk.index} の処理に失敗しました。スキップします: ${chunkError.message}`
+                  `Failed to process text block ${chunk.index}. Skipping: ${chunkError.message}`
                 );
                 continue;
               }
             }
 
-            this.logger.log(`単一テキスト処理完了: ${chunks.length} チャンク`);
+            this.logger.log(`Single text processing completed: ${chunks.length} chunks`);
           } else {
-            // その他のエラーは直接スロー
+            // Throw other errors directly
             throw error;
           }
         }
       } else {
-        // 小さなファイル、一括処理(ただしバッチ制限の確認が必要)
+        // Small files, batch processing (but need to check batch limits)
         const chunkTexts = chunks.map((chunk) => chunk.content);
 
-        // チャンク数がモデルのバッチ制限を超える場合は、強制的にバッチ処理
+        // Force batch processing if chunk count exceeds model batch limit
         if (chunks.length > recommendedBatchSize) {
           this.logger.warn(
             this.i18nService.formatMessage('chunkLimitExceededForceBatch', { actual: chunks.length, limit: recommendedBatchSize })
@@ -870,7 +870,7 @@ export class KnowledgeBaseService {
                   const embedding = embeddings[i];
 
                   if (!embedding || embedding.length === 0) {
-                    this.logger.warn(`空ベクトルのテキストブロック ${chunk.index} をスキップします`);
+                    this.logger.warn(`Skipping empty vector text block ${chunk.index}`);
                     continue;
                   }
 
@@ -893,17 +893,17 @@ export class KnowledgeBaseService {
               },
             );
           } catch (error) {
-            // コンテキスト長エラーを検出(日本語・中国語・英語に対応)
-            if (error.message && (error.message.includes('context length') || error.message.includes('コンテキスト長が上限を超えています') || error.message.includes('コンテキスト長が上限を超えています'))) {
+            // Detect context length error (supports Japanese/Chinese/English)
+            if (error.message && (error.message.includes('context length') || error.message.includes('context length exceeded'))) {
               this.logger.warn(this.i18nService.getMessage('batchContextLengthErrorFallback'));
 
-              // 単一テキスト処理にダウングレード
+              // Downgrade to single text processing
               for (let i = 0; i < chunks.length; i++) {
                 const chunk = chunks[i];
 
                 try {
                   const embeddings = await this.embeddingService.getEmbeddings(
-                    [chunk.content], // 単一テキスト
+                    [chunk.content], // Single text
                     userId,
                     kb.embeddingModelId,
                   );
@@ -930,7 +930,7 @@ export class KnowledgeBaseService {
                   );
 
                   if ((i + 1) % 10 === 0) {
-                    this.logger.log(`単一処理進捗: ${i + 1}/${chunks.length}`);
+                    this.logger.log(`Single processing progress: ${i + 1}/${chunks.length}`);
                   }
                 } catch (chunkError) {
                   this.logger.error(
@@ -942,12 +942,12 @@ export class KnowledgeBaseService {
 
               this.logger.log(this.i18nService.formatMessage('singleTextProcessingComplete', { count: chunks.length }));
             } else {
-              // その他のエラー、直接スロー
+              // Throw other errors directly
               throw error;
             }
           }
         } else {
-          // 十分に小さいファイルの場合は一括で処理
+          // Process if file is small enough
           try {
             const embeddings = await this.embeddingService.getEmbeddings(
               chunkTexts,
@@ -981,23 +981,23 @@ export class KnowledgeBaseService {
               );
             }
           } catch (error) {
-            // コンテキスト長エラーを検出(日本語・中国語・英語に対応)
-            if (error.message && (error.message.includes('context length') || error.message.includes('コンテキスト長が上限を超えています') || error.message.includes('コンテキスト長が上限を超えています'))) {
+            // Detect context length error (supports Japanese/Chinese/English)
+            if (error.message && (error.message.includes('context length') || error.message.includes('context length exceeded'))) {
               this.logger.warn(this.i18nService.getMessage('batchContextLengthErrorFallback'));
 
-              // 単一テキスト処理にダウングレード
+              // Downgrade to single text processing
               for (let i = 0; i < chunks.length; i++) {
                 const chunk = chunks[i];
 
                 try {
                   const embeddings = await this.embeddingService.getEmbeddings(
-                    [chunk.content], // 単一テキスト
+                    [chunk.content], // Single text
                     userId,
                     kb.embeddingModelId,
                   );
 
                   if (!embeddings[0] || embeddings[0].length === 0) {
-                    this.logger.warn(`空ベクトルのテキストブロック ${chunk.index} をスキップします`);
+                    this.logger.warn(`Skipping empty vector text block ${chunk.index}`);
                     continue;
                   }
 
@@ -1018,11 +1018,11 @@ export class KnowledgeBaseService {
                   );
 
                   if ((i + 1) % 10 === 0) {
-                    this.logger.log(`単一処理進捗: ${i + 1}/${chunks.length}`);
+                    this.logger.log(`Single processing progress: ${i + 1}/${chunks.length}`);
                   }
                 } catch (chunkError) {
                   this.logger.error(
-                    `テキストブロック ${chunk.index} の処理に失敗しました。スキップします: ${chunkError.message}`
+                    `Failed to process text block ${chunk.index}. Skipping: ${chunkError.message}`
                   );
                   continue;
                 }
@@ -1030,7 +1030,7 @@ export class KnowledgeBaseService {
 
               this.logger.log(this.i18nService.formatMessage('singleTextProcessingComplete', { count: chunks.length }));
             } else {
-              // その他のエラー、直接スロー
+              // Throw other errors directly
               throw error;
             }
           }
@@ -1045,7 +1045,7 @@ export class KnowledgeBaseService {
     } catch (error) {
       this.logger.error(this.i18nService.formatMessage('fileVectorizationFailed', { id: kbId }), error);
 
-      // エラー情報を metadata に保存
+      // Save error info to metadata
       try {
         const kb = await this.kbRepository.findOne({ where: { id: kbId } });
         if (kb) {
@@ -1063,7 +1063,7 @@ export class KnowledgeBaseService {
   }
 
   /**
-   * バッチ処理、メモリ制御付き
+   * Batch processing with memory control
    */
   private async processInBatches<T>(
     items: T[],
@@ -1084,19 +1084,19 @@ export class KnowledgeBaseService {
     const totalBatches = Math.ceil(totalItems / initialBatchSize);
 
     for (let i = 0; i < totalItems;) {
-      // メモリを確認し待機
+      // Check memory and wait
       await this.memoryMonitor.waitForMemoryAvailable();
 
-      // バッチサイズを動的に調整 (initialBatchSize から開始し、必要に応じてメモリモニターが削減できるようにします)
-      // 注意: memoryMonitor.getDynamicBatchSize はメモリ状況に基づいてより大きな値を返す可能性がありますが、
-      // モデルの制限 (initialBatchSize) を尊重する必要があります。
+      // Dynamically adjust batch size (start from initialBatchSize, memory monitor can reduce if needed)
+      // Note: memoryMonitor.getDynamicBatchSize may return larger values based on memory situation,
+      // but we must respect model limits (initialBatchSize)
       const currentMem = this.memoryMonitor.getMemoryUsage().heapUsed;
       const dynamicBatchSize = this.memoryMonitor.getDynamicBatchSize(currentMem);
 
       // Ensure we don't exceed the model's limit (initialBatchSize) even if memory allows more
       const batchSize = Math.min(dynamicBatchSize, initialBatchSize);
 
-      // 現在のバッチを取得
+      // Get current batch
       const batch = items.slice(i, i + batchSize);
       const batchIndex = Math.floor(i / batchSize) + 1;
 
@@ -1104,20 +1104,20 @@ export class KnowledgeBaseService {
         this.i18nService.formatMessage('batchProcessingProgress', { index: batchIndex, total: totalBatches, count: batch.length })
       );
 
-      // バッチを処理
+      // Process batch
       await processor(batch, batchIndex);
 
-      // コールバック通知
+      // Callback notification
       if (options?.onBatchComplete) {
         options.onBatchComplete(batchIndex, totalBatches);
       }
 
-      // 強制GC(メモリがしきい値に近い場合)
+      // Force GC (if memory is near threshold)
       if (currentMem > 800) {
         this.memoryMonitor.forceGC();
       }
 
-      // 参照をクリアしGCを助ける
+      // Clear references to help GC
       batch.length = 0;
 
       i += batchSize;
@@ -1128,7 +1128,7 @@ export class KnowledgeBaseService {
   }
 
   /**
-   * 失敗したファイルのベクトル化を再試行
+   * Retry vectorization for failed files
    */
   async retryFailedFile(fileId: string, userId: string, tenantId: string): Promise<KnowledgeBase> {
     this.logger.log(`Retrying failed file ${fileId} for user ${userId} in tenant ${tenantId}`);
@@ -1139,7 +1139,7 @@ export class KnowledgeBaseService {
     });
 
     if (!kb) {
-      throw new NotFoundException('ファイルが存在しません');
+      throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
     }
 
     if (kb.status !== FileStatus.FAILED) {
@@ -1150,10 +1150,10 @@ export class KnowledgeBaseService {
       throw new Error(this.i18nService.getMessage('emptyFileRetryFailed'));
     }
 
-    // 2. ステータスを INDEXING にリセット
+    // 2. Reset status to INDEXING
     await this.updateStatus(fileId, FileStatus.INDEXING);
 
-    // 3. 非同期でベクトル化をトリガー(既存ロジックを再利用)
+    // 3. Trigger vectorization asynchronously (reuse existing logic)
     this.vectorizeToElasticsearch(
       fileId,
       userId,
@@ -1168,16 +1168,16 @@ export class KnowledgeBaseService {
       this.logger.error(`Retry vectorization failed for file ${fileId}`, err);
     });
 
-    // 4. 更新後のファイルステータスを返却
+    // 4. Return updated file status
     const updatedKb = await this.kbRepository.findOne({ where: { id: fileId, tenantId } });
     if (!updatedKb) {
-      throw new NotFoundException('ファイルが存在しません');
+      throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
     }
     return updatedKb;
   }
 
   /**
-   * ファイルのすべてのチャンク情報を取得
+   * Get all chunk information for a file
    */
   async getFileChunks(fileId: string, userId: string, tenantId: string) {
     this.logger.log(`Getting chunks for file ${fileId}, user ${userId}, tenant ${tenantId}`);
@@ -1188,13 +1188,13 @@ export class KnowledgeBaseService {
     });
 
     if (!kb) {
-      throw new NotFoundException('ファイルが存在しません');
+      throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
     }
 
-    // 2. Elasticsearch からすべてのチャンクを取得
+    // 2. Get all chunks from Elasticsearch
     const chunks = await this.elasticsearchService.getFileChunks(fileId, userId, tenantId);
 
-    // 3. チャンク情報を返却
+    // 3. Return chunk info
     return {
       fileId: kb.id,
       fileName: kb.originalName,
@@ -1215,7 +1215,7 @@ export class KnowledgeBaseService {
     await this.kbRepository.update(id, { status });
   }
 
-  // PDF プレビュー関連メソッド
+  // PDF preview related methods
   async ensurePDFExists(fileId: string, userId: string, tenantId: string, force: boolean = false): Promise<string> {
     const kb = await this.kbRepository.findOne({
       where: { id: fileId, tenantId },
@@ -1225,12 +1225,12 @@ export class KnowledgeBaseService {
       throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
     }
 
-    // 元ファイルが PDF の場合は、元ファイルのパスを直接返す
+    // If original file is PDF, return the original file path directly
     if (kb.mimetype === 'application/pdf') {
       return kb.storagePath;
     }
 
-    // プレビュー変換に対応しているか確認(ドキュメント類または画像類のみ許可)
+    // Check if preview conversion is supported (only documents or images allowed)
     const ext = kb.originalName.toLowerCase().split('.').pop() || '';
     const isConvertible = [...DOC_EXTENSIONS, ...IMAGE_EXTENSIONS].includes(ext);
 
@@ -1239,14 +1239,14 @@ export class KnowledgeBaseService {
       throw new Error(this.i18nService.getMessage('pdfPreviewNotSupported'));
     }
 
-    // PDF フィールドパスを生成
+    // Generate PDF field path
     const path = await import('path');
     const fs = await import('fs');
     const uploadDir = path.dirname(kb.storagePath);
     const baseName = path.basename(kb.storagePath, path.extname(kb.storagePath));
     const pdfPath = path.join(uploadDir, `${baseName}.pdf`);
 
-    // 強制再生成が指定され、ファイルが存在する場合は削除
+    // Delete if forced regeneration specified and file exists
     if (force && fs.existsSync(pdfPath)) {
       try {
         fs.unlinkSync(pdfPath);
@@ -1256,7 +1256,7 @@ export class KnowledgeBaseService {
       }
     }
 
-    // 変換済みかつ強制再生成が不要か確認
+    // Check if already converted and regeneration not needed
     if (fs.existsSync(pdfPath) && !force) {
       if (!kb.pdfPath) {
         await this.kbRepository.update(kb.id, { pdfPath: pdfPath });
@@ -1264,14 +1264,14 @@ export class KnowledgeBaseService {
       return pdfPath;
     }
 
-    // PDF への変換が必要
+    // Need to convert to PDF
     try {
       this.logger.log(`Starting PDF conversion for ${kb.originalName} at ${kb.storagePath}`);
 
-      // ファイルを変換
+      // Convert file
       await this.libreOfficeService.convertToPDF(kb.storagePath);
 
-      // 変換結果を確認
+      // Check conversion result
       if (!fs.existsSync(pdfPath)) {
         throw new Error(`PDF conversion completed but file not found at ${pdfPath}`);
       }
@@ -1301,7 +1301,7 @@ export class KnowledgeBaseService {
       throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
     }
 
-    // 元ファイルが PDF の場合
+    // If original file is PDF
     if (kb.mimetype === 'application/pdf') {
       const token = this.generateTempToken(fileId, userId, tenantId);
       return {
@@ -1310,14 +1310,14 @@ export class KnowledgeBaseService {
       };
     }
 
-    // PDF ファイルパスを生成
+    // Generate PDF file path
     const path = await import('path');
     const fs = await import('fs');
     const uploadDir = path.dirname(kb.storagePath);
     const baseName = path.basename(kb.storagePath, path.extname(kb.storagePath));
     const pdfPath = path.join(uploadDir, `${baseName}.pdf`);
 
-    // 変換済みか確認
+    // Check if converted
     if (fs.existsSync(pdfPath)) {
       if (!kb.pdfPath) {
         kb.pdfPath = pdfPath;
@@ -1330,7 +1330,7 @@ export class KnowledgeBaseService {
       };
     }
 
-    // 変換が必要
+    // Conversion needed
     return {
       status: 'pending',
     };
@@ -1341,7 +1341,7 @@ export class KnowledgeBaseService {
 
     const secret = process.env.JWT_SECRET;
     if (!secret) {
-      throw new Error('JWT_SECRET environment variable is required but not set');
+      throw new Error(this.i18nService.getMessage('jwtSecretRequired'));
     }
 
     return jwt.sign(
@@ -1352,7 +1352,7 @@ export class KnowledgeBaseService {
   }
 
   /**
-   * モデルの実際の次元数を取得(キャッシュ確認とプローブロジック付き)
+   * Get actual model dimensions (with cache check and probe logic)
    */
   private async getActualModelDimensions(embeddingModelId: string, userId: string, tenantId: string): Promise<number> {
     const defaultDimensions = parseInt(
@@ -1360,7 +1360,7 @@ export class KnowledgeBaseService {
     );
 
     try {
-      // 1. モデル設定から優先的に取得
+      // 1. Prioritize getting from model config
       const modelConfig = await this.modelConfigService.findOne(
         embeddingModelId,
         userId,
@@ -1368,12 +1368,12 @@ export class KnowledgeBaseService {
       );
 
       if (modelConfig && modelConfig.dimensions) {
-        this.logger.log(`設定から ${modelConfig.name} の次元数を取得しました: ${modelConfig.dimensions}`);
+        this.logger.log(`Got dimensions from ${modelConfig.name} config: ${modelConfig.dimensions}`);
         return modelConfig.dimensions;
       }
 
-      // 2. それ以外の場合はプローブにより取得
-      this.logger.log(`モデル次元数をプローブ中: ${embeddingModelId}`);
+      // 2. Otherwise probe for dimensions
+      this.logger.log(`Probing model dimensions: ${embeddingModelId}`);
       const probeEmbeddings = await this.embeddingService.getEmbeddings(
         ['probe'],
         userId,
@@ -1382,17 +1382,17 @@ export class KnowledgeBaseService {
 
       if (probeEmbeddings.length > 0) {
         const actualDimensions = probeEmbeddings[0].length;
-        this.logger.log(`モデルの実際の次元数を検出しました: ${actualDimensions}`);
+        this.logger.log(`Detected actual model dimensions: ${actualDimensions}`);
 
-        // 次回利用のためにモデル設定を更新
+        // Update model config for next use
         if (modelConfig) {
           try {
             await this.modelConfigService.update(userId, tenantId, modelConfig.id, {
               dimensions: actualDimensions,
             });
-            this.logger.log(`モデル ${modelConfig.name} の次元数設定を ${actualDimensions} に更新しました`);
+            this.logger.log(`Updated model ${modelConfig.name} dimension config to ${actualDimensions}`);
           } catch (updateErr) {
-            this.logger.warn(`モデル次元数設定の更新に失敗しました: ${updateErr.message}`);
+            this.logger.warn(`Failed to update model dimension config: ${updateErr.message}`);
           }
         }
 
@@ -1400,7 +1400,7 @@ export class KnowledgeBaseService {
       }
     } catch (err) {
       this.logger.warn(
-        `次元数の取得に失敗しました。デフォルト次元数を使用します: ${defaultDimensions}`,
+        `Failed to get dimensions. Using default: ${defaultDimensions}`,
         err.message,
       );
     }
@@ -1409,7 +1409,7 @@ export class KnowledgeBaseService {
   }
 
   /**
-   * AIを使用して文書のタイトルを自動生成する
+   * Auto-generate document title using AI
    */
   async generateTitle(kbId: string): Promise<string | null> {
     this.logger.log(`Generating automatic title for file ${kbId}`);
@@ -1421,22 +1421,22 @@ export class KnowledgeBaseService {
       }
       const tenantId = kb.tenantId;
 
-      // すでにタイトルがある場合はスキップ
+      // Skip if title already exists
       if (kb.title) {
         return kb.title;
       }
 
-      // コンテンツの冒頭サンプルを取得(最大2500文字)
+      // Get content sample (max 2500 characters)
       const contentSample = kb.content.substring(0, 2500);
 
-      // 組織設定から言語を取得、またはデフォルトを使用
+      // Get language from org settings, or use default
       const userSettings = await this.userSettingService.getByUser(kb.userId);
       const language = userSettings.language || 'zh';
 
-      // プロンプトを構築
+      // Build prompt
       const prompt = this.i18nService.getDocumentTitlePrompt(language, contentSample);
 
-      // LLMを呼び出してタイトルを生成
+      // Call LLM to generate title
       let generatedTitle: string | undefined;
       try {
         generatedTitle = await this.chatService.generateSimpleChat(
@@ -1450,11 +1450,11 @@ export class KnowledgeBaseService {
       }
 
       if (generatedTitle && generatedTitle.trim().length > 0) {
-        // 余分な引用符や改行を除去
+        // Remove extra quotes and newlines
         const cleanedTitle = generatedTitle.trim().replace(/^["']|["']$/g, '').substring(0, 100);
         await this.kbRepository.update(kbId, { title: cleanedTitle });
 
-        // Elasticsearch のチャンクも更新
+        // Also update ES chunks
         await this.elasticsearchService.updateTitleByFileId(kbId, cleanedTitle, tenantId).catch((err) => {
           this.logger.error(`Failed to update title in Elasticsearch for ${kbId}`, err);
         });

+ 46 - 46
server/src/knowledge-base/memory-monitor.service.ts

@@ -1,10 +1,10 @@
 import { Injectable, Logger } from '@nestjs/common';
 
 export interface MemoryStats {
-  heapUsed: number;    // 使用済みヒープメモリ (MB)
-  heapTotal: number;   // 総ヒープメモリ (MB)
-  external: number;    // 外部メモリ (MB)
-  rss: number;         // RSS (常駐セットサイズ) (MB)
+  heapUsed: number;    // Used heap memory (MB)
+  heapTotal: number;   // Total heap memory (MB)
+  external: number;    // External memory (MB)
+  rss: number;         // RSS (Resident Set Size) (MB)
   timestamp: Date;
 }
 
@@ -16,16 +16,16 @@ export class MemoryMonitorService {
   private readonly GC_THRESHOLD_MB: number;
 
   constructor() {
-    // 環境変数から設定を読み込む。デフォルト値はメモリ最適化用
-    this.MAX_MEMORY_MB = parseInt(process.env.MAX_MEMORY_USAGE_MB || '1024'); // 1GB上限
-    this.BATCH_SIZE = parseInt(process.env.CHUNK_BATCH_SIZE || '100'); // 1バッチあたり100チャンク
-    this.GC_THRESHOLD_MB = parseInt(process.env.GC_THRESHOLD_MB || '800'); // 800MBでGCをトリガー
+    // Load config from env vars. Default values for memory optimization
+    this.MAX_MEMORY_MB = parseInt(process.env.MAX_MEMORY_USAGE_MB || '1024'); // 1GB limit
+    this.BATCH_SIZE = parseInt(process.env.CHUNK_BATCH_SIZE || '100'); // 100 chunks per batch
+    this.GC_THRESHOLD_MB = parseInt(process.env.GC_THRESHOLD_MB || '800'); // Trigger GC at 800MB
 
-    this.logger.log(`メモリ監視を初期化しました: 上限=${this.MAX_MEMORY_MB}MB, バッチサイズ=${this.BATCH_SIZE}, GC閾値=${this.GC_THRESHOLD_MB}MB`);
+    this.logger.log(`Memory monitor initialized: limit=${this.MAX_MEMORY_MB}MB, batchSize=${this.BATCH_SIZE}, GCThreshold=${this.GC_THRESHOLD_MB}MB`);
   }
 
   /**
-   * 現在のメモリ使用状況を取得
+   * Get current memory usage
    */
   getMemoryUsage(): MemoryStats {
     const usage = process.memoryUsage();
@@ -39,31 +39,31 @@ export class MemoryMonitorService {
   }
 
   /**
-   * メモリが上限に近づいているかチェック
+   * Check if memory is approaching limit
    */
   isMemoryHigh(): boolean {
     const usage = this.getMemoryUsage();
-    return usage.heapUsed > this.MAX_MEMORY_MB * 0.85; // 85%閾値
+    return usage.heapUsed > this.MAX_MEMORY_MB * 0.85; // 85% threshold
   }
 
   /**
-   * メモリが利用可能になるまで待機(タイムアウトあり)
+   * Wait for memory to become available (with timeout)
    */
   async waitForMemoryAvailable(timeoutMs: number = 30000): Promise<void> {
     const startTime = Date.now();
 
     while (this.isMemoryHigh()) {
       if (Date.now() - startTime > timeoutMs) {
-        throw new Error(`メモリ待機がタイムアウトしました: 現在 ${this.getMemoryUsage().heapUsed}MB > ${this.MAX_MEMORY_MB * 0.85}MB`);
+        throw new Error(`Memory wait timeout: current ${this.getMemoryUsage().heapUsed}MB > ${this.MAX_MEMORY_MB * 0.85}MB`);
       }
 
       this.logger.warn(
-        `メモリ使用量が高すぎます。解放を待機中... ${this.getMemoryUsage().heapUsed}/${this.MAX_MEMORY_MB}MB`,
+        `Memory usage too high. Waiting for release... ${this.getMemoryUsage().heapUsed}/${this.MAX_MEMORY_MB}MB`,
       );
 
-      // ガベージコレクションを強制実行(可能な場合)
+      // Force garbage collection (if available)
       if (global.gc) {
-        this.logger.log('強制ガベージコレクションを実行中...');
+        this.logger.log('Running forced garbage collection...');
         global.gc();
       }
 
@@ -72,7 +72,7 @@ export class MemoryMonitorService {
   }
 
   /**
-   * ガベージコレクションを強制実行(可能な場合)
+   * Force garbage collection (if available)
    */
   forceGC(): void {
     if (global.gc) {
@@ -80,30 +80,30 @@ export class MemoryMonitorService {
       global.gc();
       const after = this.getMemoryUsage();
       this.logger.log(
-        `GC完了: ${before.heapUsed}MB → ${after.heapUsed}MB (${before.heapUsed - after.heapUsed}MB 解放)`,
+        `GC completed: ${before.heapUsed}MB → ${after.heapUsed}MB (${before.heapUsed - after.heapUsed}MB freed)`,
       );
     }
   }
 
   /**
-   * バッチサイズを動的に調整
+   * Dynamically adjust batch size
    */
   getDynamicBatchSize(currentMemoryMB: number): number {
     const baseBatchSize = this.BATCH_SIZE;
 
     if (currentMemoryMB > this.GC_THRESHOLD_MB) {
-      // メモリ逼迫、バッチサイズを削減
+      // Memory pressure, reduce batch size
       const reduced = Math.max(10, Math.floor(baseBatchSize * 0.5));
       this.logger.warn(
-        `メモリ逼迫 (${currentMemoryMB}MB)、バッチサイズを動的に調整: ${baseBatchSize} → ${reduced}`,
+        `Memory pressure (${currentMemoryMB}MB), adjusting batch size: ${baseBatchSize} → ${reduced}`,
       );
       return reduced;
     } else if (currentMemoryMB < this.MAX_MEMORY_MB * 0.4) {
-      // メモリに余裕あり、バッチサイズを増量
+      // Enough memory, increase batch size
       const increased = Math.min(200, Math.floor(baseBatchSize * 1.2));
       if (increased > baseBatchSize) {
         this.logger.log(
-          `メモリに余裕あり (${currentMemoryMB}MB)、バッチサイズを動的に調整: ${baseBatchSize} → ${increased}`,
+          `Memory available (${currentMemoryMB}MB), adjusting batch size: ${baseBatchSize} → ${increased}`,
         );
       }
       return increased;
@@ -113,7 +113,7 @@ export class MemoryMonitorService {
   }
 
   /**
-   * 大規模データの処理:自動バッチングとメモリ制御
+   * Process large data: auto-batching and memory control
    */
   async processInBatches<T, R>(
     items: T[],
@@ -127,44 +127,44 @@ export class MemoryMonitorService {
     if (totalItems === 0) return [];
 
     const startTime = Date.now();
-    this.logger.log(`バッチ処理を開始します: ${totalItems} 項目`);
+    this.logger.log(`Starting batch processing: ${totalItems} items`);
 
     const allResults: R[] = [];
     let processedCount = 0;
 
     for (let i = 0; i < totalItems;) {
-      // メモリ状態をチェックして待機
+      // Check memory state and wait
       await this.waitForMemoryAvailable();
 
-      // バッチサイズを動的に調整
+      // Dynamically adjust batch size
       const currentMem = this.getMemoryUsage().heapUsed;
       const batchSize = this.getDynamicBatchSize(currentMem);
 
-      // 現在のバッチを取得
+      // Get current batch
       const batch = items.slice(i, i + batchSize);
       const batchIndex = Math.floor(i / batchSize) + 1;
       const totalBatches = Math.ceil(totalItems / batchSize);
 
       this.logger.log(
-        `バッチを処理中 ${batchIndex}/${totalBatches}: ${batch.length} 項目 (累計 ${processedCount}/${totalItems})`,
+        `Processing batch ${batchIndex}/${totalBatches}: ${batch.length} items (cumulative ${processedCount}/${totalItems})`,
       );
 
-      // バッチを処理
+      // Process batch
       const batchResults = await processor(batch, batchIndex);
       allResults.push(...batchResults);
       processedCount += batch.length;
 
-      // コールバック通知
+      // Callback notification
       if (options?.onBatchComplete) {
         await options.onBatchComplete(batchIndex, totalBatches, batchResults);
       }
 
-      // メモリが閾値に近い場合はGCを強制実行
+      // Force GC if memory near threshold
       if (currentMem > this.GC_THRESHOLD_MB) {
         this.forceGC();
       }
 
-      // GCを助けるために参照をクリア
+      // Clear references to help GC
       batch.length = 0;
 
       i += batchSize;
@@ -173,23 +173,23 @@ export class MemoryMonitorService {
     const duration = ((Date.now() - startTime) / 1000).toFixed(2);
     const finalMem = this.getMemoryUsage();
     this.logger.log(
-      `バッチ処理が完了しました: ${totalItems} 項目, 所要時間 ${duration}s, 最終メモリ ${finalMem.heapUsed}MB`,
+      `Batch processing completed: ${totalItems} items, duration ${duration}s, final memory ${finalMem.heapUsed}MB`,
     );
 
     return allResults;
   }
 
   /**
-   * 処理に必要なメモリを見積もる
+   * Estimate memory required for processing
    */
   estimateMemoryUsage(itemCount: number, itemSizeBytes: number, vectorDim: number): number {
-    // テキスト内容のメモリ
+    // Text content memory
     const textMemory = itemCount * itemSizeBytes;
 
-    // ベクトルメモリ (各ベクトル: 次元 × 4バイト)
+    // Vector memory (dimension * 4 bytes per vector)
     const vectorMemory = itemCount * vectorDim * 4;
 
-    // オブジェクトのオーバーヘッド (各オブジェクトにつき追加で約100バイトと推定)
+    // Object overhead (~100 bytes per object)
     const overhead = itemCount * 100;
 
     const totalMB = Math.round((textMemory + vectorMemory + overhead) / 1024 / 1024);
@@ -198,15 +198,15 @@ export class MemoryMonitorService {
   }
 
   /**
-   * バッチ処理を使用すべきかチェック
+   * Check if batching should be used
    */
   shouldUseBatching(itemCount: number, itemSizeBytes: number, vectorDim: number): boolean {
     const estimatedMB = this.estimateMemoryUsage(itemCount, itemSizeBytes, vectorDim);
-    const threshold = this.MAX_MEMORY_MB * 0.7; // 70%閾値
+    const threshold = this.MAX_MEMORY_MB * 0.7; // 70% threshold
 
     if (estimatedMB > threshold) {
       this.logger.warn(
-        `推定メモリ ${estimatedMB}MB が閾値 ${threshold}MB を超えているため、バッチ処理を使用します`,
+        `Estimated memory ${estimatedMB}MB exceeds threshold ${threshold}MB, using batch processing`,
       );
       return true;
     }
@@ -215,19 +215,19 @@ export class MemoryMonitorService {
   }
 
   /**
-   * 推奨されるバッチサイズを取得
+   * Get recommended batch size
    */
   getRecommendedBatchSize(itemSizeBytes: number, vectorDim: number): number {
-    // 目標:1バッチあたり最大 200MB メモリ
+    // Goal: max 200MB memory per batch
     const targetMemoryMB = 200;
     const targetMemoryBytes = targetMemoryMB * 1024 * 1024;
 
-    // 1項目あたりのメモリ = テキスト + ベクトル + オーバーヘッド
+    // Memory per item = text + vector + overhead
     const singleItemMemory = itemSizeBytes + (vectorDim * 4) + 100;
 
     const batchSize = Math.floor(targetMemoryBytes / singleItemMemory);
 
-    // 限制在 10-200 之间
+    // Limit between 10-200
     return Math.max(10, Math.min(200, batchSize));
   }
 }

+ 3 - 3
server/src/knowledge-base/text-chunker.service.ts

@@ -22,7 +22,7 @@ export class TextChunkerService {
     const chunkSizeInChars = chunkSize * 4; // 1 token ≈ 4 chars
     const overlapInChars = overlap * 4;
 
-    // テキスト長がチャンクサイズ以下の場合は、テキスト全体を1つのチャンクとして直接返す
+    // If text length <= chunk size, return entire text as one chunk
     if (cleanText.length <= chunkSizeInChars) {
       return [
         {
@@ -41,7 +41,7 @@ export class TextChunkerService {
     while (start < cleanText.length) {
       let end = Math.min(start + chunkSizeInChars, cleanText.length);
 
-      // 文の境界で分割
+      // Split by sentence boundaries
       if (end < cleanText.length) {
         const sentenceEnd = this.findSentenceEnd(
           cleanText,
@@ -69,7 +69,7 @@ export class TextChunkerService {
         break;
       }
 
-      // 次のチャンクの開始位置を計算
+      // Calculate start position of next chunk
       const newStart = end - overlapInChars;
       // Protect against infinite loop if overlap is too large or chunk too small
       if (newStart <= start) {

+ 6 - 2
server/src/knowledge-group/knowledge-group.controller.ts

@@ -14,11 +14,15 @@ import { RolesGuard } from '../auth/roles.guard';
 import { Roles } from '../auth/roles.decorator';
 import { UserRole } from '../user/user-role.enum';
 import { KnowledgeGroupService, CreateGroupDto, UpdateGroupDto } from './knowledge-group.service';
+import { I18nService } from '../i18n/i18n.service';
 
 @Controller('knowledge-groups')
 @UseGuards(CombinedAuthGuard, RolesGuard)
 export class KnowledgeGroupController {
-  constructor(private readonly groupService: KnowledgeGroupService) { }
+  constructor(
+    private readonly groupService: KnowledgeGroupService,
+    private readonly i18nService: I18nService,
+  ) { }
 
   @Get()
   async findAll(@Request() req) {
@@ -51,7 +55,7 @@ export class KnowledgeGroupController {
   @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
   async remove(@Param('id') id: string, @Request() req) {
     await this.groupService.remove(id, req.user.id, req.user.tenantId);
-    return { message: 'Group deleted successfully' };
+    return { message: this.i18nService.getMessage('groupDeleted') };
   }
 
   @Get(':id/files')

+ 2 - 2
server/src/libreoffice/libreoffice.interface.ts

@@ -1,10 +1,10 @@
 /**
- * LibreOffice サービスインターフェース定義
+ * LibreOffice Service Interface Definition
  */
 
 export interface LibreOfficeConvertResponse {
   pdf_path?: string;
-  pdf_data?: string; // base64 エンコードされた PDF データ
+  pdf_data?: string; // base64 encoded PDF data
   converted: boolean;
   original: string;
   file_size: number;

+ 36 - 32
server/src/libreoffice/libreoffice.service.ts

@@ -5,25 +5,29 @@ import * as path from 'path';
 import axios from 'axios';
 import FormData from 'form-data';
 import { LibreOfficeConvertResponse, LibreOfficeHealthResponse } from './libreoffice.interface';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class LibreOfficeService implements OnModuleInit {
   private readonly logger = new Logger(LibreOfficeService.name);
   private baseUrl: string;
 
-  constructor(private configService: ConfigService) { }
+  constructor(
+    private configService: ConfigService,
+    private i18nService: I18nService,
+  ) { }
 
   onModuleInit() {
     const libreofficeUrl = this.configService.get<string>('LIBREOFFICE_URL');
     if (!libreofficeUrl) {
-      throw new Error('LIBREOFFICE_URL environment variable is required but not set');
+      throw new Error(this.i18nService.getMessage('libreofficeUrlRequired'));
     }
     this.baseUrl = libreofficeUrl;
     this.logger.log(`LibreOffice service initialized with base URL: ${this.baseUrl}`);
   }
 
   /**
-   * LibreOffice サービスの状態をチェック
+   * Check LibreOffice service health status
    */
   async healthCheck(): Promise<boolean> {
     try {
@@ -39,69 +43,69 @@ export class LibreOfficeService implements OnModuleInit {
   }
 
   /**
-   * ドキュメントを PDF に変換
-   * @param filePath 変換するファイルのパス
-   * @returns PDF ファイルのパス
+   * Convert document to PDF
+   * @param filePath Path of file to convert
+   * @returns PDF file path
    */
   async convertToPDF(filePath: string): Promise<string> {
     const fileName = path.basename(filePath);
     const ext = path.extname(fileName).toLowerCase();
 
-    // PDF の場合は元のパスを直接返す
+    // Return original path directly if PDF
     if (ext === '.pdf') {
       this.logger.log(`File is already PDF: ${filePath}`);
       return filePath;
     }
 
-    // ファイルの存在確認
+    // Check if file exists
     try {
       await fs.access(filePath);
     } catch {
-      throw new Error(`ファイルが存在しません: ${filePath}`);
+      throw new Error(`File does not exist: ${filePath}`);
     }
 
-    // 出力先 PDF のパスを生成
+    // Generate output PDF path
     const dir = path.dirname(filePath);
     const baseName = path.basename(filePath, ext);
     const targetPdfPath = path.join(dir, `${baseName}.pdf`);
 
-    // PDF が既に存在する場合は直接返す
+    // Return directly if PDF already exists
     try {
       await fs.access(targetPdfPath);
       this.logger.log(`PDF already exists: ${targetPdfPath}`);
       return targetPdfPath;
     } catch {
-      // PDF が存在しないため、変換が必要
+      // Need to convert as PDF does not exist
     }
 
-    // ファイルの読み込み
+    // Load file
     const fileBuffer = await fs.readFile(filePath);
 
-    // FormData の構築
+    // Build FormData
     const formData = new FormData();
     formData.append('file', fileBuffer, fileName);
 
     this.logger.log(`Converting ${fileName} to PDF...`);
 
-    // 変換の再試行回数
+    // Conversion retry count
     const maxRetries = 3;
     let lastError: any;
 
     for (let attempt = 1; attempt <= maxRetries; attempt++) {
       try {
-        // LibreOffice サービスの呼び出し
+        // Call LibreOffice service
         const response = await axios.post(
           `${this.baseUrl}/convert`,
           formData,
           {
             headers: formData.getHeaders(),
-            timeout: 300000, // 5分タイムアウト
-            responseType: 'stream', // ファイルストリームを受信
-            maxRedirects: 5, // リダイレクトの最大数
+            timeout: 300000, // 5 minute timeout
+            responseType: 'stream', // Receive file stream
+            maxRedirects: 5, // Max redirects
           }
         );
 
-        // ストリームを出力ファイルに書き込む
+        // Write stream to output file
         const writer = (await import('fs')).createWriteStream(targetPdfPath);
         response.data.pipe(writer);
 
@@ -120,21 +124,21 @@ export class LibreOfficeService implements OnModuleInit {
         this.logger.error(`Attempt ${attempt} failed for ${fileName}: ${error.message}`);
         lastError = error;
 
-        // socket hang up や接続エラーの場合は少し待機して再試行
+        // Wait and retry on socket hang up or connection error
         if (error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.message.includes('socket hang up')) {
           if (attempt < maxRetries) {
-            const delay = 2000 * attempt; // だんだん増える遅延
+            const delay = 2000 * attempt; // Increasing delay
             this.logger.log(`Waiting ${delay}ms before retry...`);
             await new Promise(resolve => setTimeout(resolve, delay));
           }
         } else {
-          // その他のエラーは再試行しない
+          // Do not retry other errors
           break;
         }
       }
     }
 
-    // 全ての再試行が失敗した場合、詳細なエラーハンドリングを行う
+    // Detailed error handling if all retries fail
     if (lastError.response) {
       try {
         const stream = lastError.response.data;
@@ -154,28 +158,28 @@ export class LibreOfficeService implements OnModuleInit {
         }
 
         if (lastError.response.status === 504) {
-          throw new Error('変換がタイムアウトしました。ファイルが大きすぎる可能性があります');
+          throw new Error('Conversion timed out. The file may be too large.');
         }
-        throw new Error(`変換に失敗しました: ${detail}`);
+        throw new Error(`Conversion failed: ${detail}`);
 
       } catch (streamError) {
         this.logger.error('Error reading error stream:', streamError);
-        throw new Error(`変換に失敗しました: ${lastError.message}`);
+        throw new Error(`Conversion failed: ${lastError.message}`);
       }
     }
 
     this.logger.error(`Conversion failed for ${fileName} after ${maxRetries} attempts:`, lastError.message);
     if (lastError.code === 'ECONNREFUSED') {
-      throw new Error('LibreOffice サービスが実行されていません。サービスの状態を確認してください');
+      throw new Error('LibreOffice service is not running. Please check the service status.');
     }
     if (lastError.code === 'ECONNRESET' || lastError.message.includes('socket hang up')) {
-      throw new Error('LibreOffice サービスとの接続が切断されました。サービスが不安定である可能性があります');
+      throw new Error('Connection to LibreOffice service was reset. The service may be unstable.');
     }
-    throw new Error(`変換に失敗しました: ${lastError.message}`);
+    throw new Error(`Conversion failed: ${lastError.message}`);
   }
 
   /**
-   * ファイルの一括変換
+   * Batch convert files
    */
   async batchConvert(filePaths: string[]): Promise<string[]> {
     const results: string[] = [];
@@ -192,7 +196,7 @@ export class LibreOfficeService implements OnModuleInit {
   }
 
   /**
-   * サービスのバージョン情報を取得
+   * Get service version information
    */
   async getVersion(): Promise<any> {
     try {

+ 9 - 9
server/src/migrations/1737800000000-AddKnowledgeBaseEnhancements.ts

@@ -4,7 +4,7 @@ export class AddKnowledgeBaseEnhancements1737800000000 implements MigrationInter
   name = 'AddKnowledgeBaseEnhancements1737800000000';
 
   public async up(queryRunner: QueryRunner): Promise<void> {
-    // 知識ベースグループテーブルの作成
+    // Create knowledge base group table
     await queryRunner.query(`
       CREATE TABLE "knowledge_groups" (
         "id" varchar PRIMARY KEY NOT NULL,
@@ -17,7 +17,7 @@ export class AddKnowledgeBaseEnhancements1737800000000 implements MigrationInter
       )
     `);
 
-    // ドキュメントグループ関連テーブルの作成
+    // Create document group related tables
     await queryRunner.query(`
       CREATE TABLE "knowledge_base_groups" (
         "knowledge_base_id" varchar NOT NULL,
@@ -29,7 +29,7 @@ export class AddKnowledgeBaseEnhancements1737800000000 implements MigrationInter
       )
     `);
 
-    // 検索履歴テーブルの作成
+    // Create search history table
     await queryRunner.query(`
       CREATE TABLE "search_history" (
         "id" varchar PRIMARY KEY NOT NULL,
@@ -41,7 +41,7 @@ export class AddKnowledgeBaseEnhancements1737800000000 implements MigrationInter
       )
     `);
 
-    // 会話メッセージテーブルの作成
+    // Create conversation message table
     await queryRunner.query(`
       CREATE TABLE "chat_messages" (
         "id" varchar PRIMARY KEY NOT NULL,
@@ -54,27 +54,27 @@ export class AddKnowledgeBaseEnhancements1737800000000 implements MigrationInter
       )
     `);
 
-    // knowledge_base テーブルに pdf_path フィールドを追加
+    // Add pdf_path field to knowledge_base table
     await queryRunner.query(`
       ALTER TABLE "knowledge_base" ADD COLUMN "pdf_path" varchar
     `);
 
-    // インデックスの作成
+    // Create index
     await queryRunner.query(`CREATE INDEX "IDX_knowledge_groups_user_id" ON "knowledge_groups" ("user_id")`);
     await queryRunner.query(`CREATE INDEX "IDX_search_history_user_id" ON "search_history" ("user_id")`);
     await queryRunner.query(`CREATE INDEX "IDX_chat_messages_search_history_id" ON "chat_messages" ("search_history_id")`);
   }
 
   public async down(queryRunner: QueryRunner): Promise<void> {
-    // インデックスの削除
+    // Delete index
     await queryRunner.query(`DROP INDEX "IDX_chat_messages_search_history_id"`);
     await queryRunner.query(`DROP INDEX "IDX_search_history_user_id"`);
     await queryRunner.query(`DROP INDEX "IDX_knowledge_groups_user_id"`);
 
-    // pdf_path フィールドの削除
+    // Delete pdf_path field
     await queryRunner.query(`ALTER TABLE "knowledge_base" DROP COLUMN "pdf_path"`);
 
-    // テーブルの削除
+    // Delete table
     await queryRunner.query(`DROP TABLE "chat_messages"`);
     await queryRunner.query(`DROP TABLE "search_history"`);
     await queryRunner.query(`DROP TABLE "knowledge_base_groups"`);

+ 10 - 10
server/src/model-config/dto/create-model-config.dto.ts

@@ -28,22 +28,22 @@ export class CreateModelConfigDto {
 
   @IsString()
   @IsOptional()
-  apiKey?: string; // APIキーはオプションです - ローカルモデルを許可します
+  apiKey?: string; // API key is optional - allows local models
 
   @IsEnum(ModelType)
   @IsNotEmpty()
   type: ModelType;
 
   @IsNumber()
-  @Min(1, { message: 'ベクトル次元の最小値は 1 です' })
-  @Max(4096, { message: 'ベクトル次元の最大値は 4096 です(Elasticsearch の制限)' })
+  @Min(1, { message: 'Minimum vector dimension is 1' })
+  @Max(4096, { message: 'Maximum vector dimension is 4096 (Elasticsearch limit)' })
   @IsOptional()
   dimensions?: number;
 
-  // ==================== 追加フィールド ====================
+  // ==================== Additional Fields ====================
 
   /**
-   * モデルの入力トークン制限(embedding/rerank にのみ有効)
+   * Model input token limit (only valid for embedding/rerank)
    */
   @IsNumber()
   @Min(1)
@@ -52,7 +52,7 @@ export class CreateModelConfigDto {
   maxInputTokens?: number;
 
   /**
-   * バッチ処理の制限(embedding/rerank にのみ有効)
+   * Batch processing limit (only valid for embedding/rerank)
    */
   @IsNumber()
   @Min(1)
@@ -61,28 +61,28 @@ export class CreateModelConfigDto {
   maxBatchSize?: number;
 
   /**
-   * ベトルモデルかどうか
+   * Whether this is a vector model
    */
   @IsBoolean()
   @IsOptional()
   isVectorModel?: boolean;
 
   /**
-   * モデルプロバイダー名
+   * Model provider name
    */
   @IsString()
   @IsOptional()
   providerName?: string;
 
   /**
-   * このモデルを有効にするかどうか
+   * Whether to enable this model
    */
   @IsBoolean()
   @IsOptional()
   isEnabled?: boolean;
 
   /**
-   * このモデルをデフォルトとして使用するかどうか
+   * Whether to use this model as default
    */
   @IsBoolean()
   @IsOptional()

+ 15 - 15
server/src/model-config/model-config.entity.ts

@@ -31,53 +31,53 @@ export class ModelConfig {
   type: string; // ModelType enum values
 
   @Column({ type: 'integer', nullable: true })
-  dimensions?: number; // 埋め込みモデルの次元、システムによって自動的に検出され保存されます
+  dimensions?: number; // Embedding model dimensions, auto-detected and saved by system
 
-  // ==================== 追加フィールド ====================
-  // 以下字段仅对 embedding/rerank 模型有意义
+  // ==================== Additional Fields ====================
+  // The following fields are only meaningful for embedding/rerank models
 
   /**
-   * モデルの入力トークン制限
-   * : OpenAI=8191, Gemini=2048
+   * Model input token limit
+   * Example: OpenAI=8191, Gemini=2048
    */
   @Column({ type: 'integer', nullable: true, default: 8191 })
   maxInputTokens?: number;
 
   /**
-   * 一括処理制限(1回のリクエストあたりの最大入力数)
-   * : OpenAI=2048, Gemini=100
+   * Batch processing limit (max inputs per request)
+   * Example: OpenAI=2048, Gemini=100
    */
   @Column({ type: 'integer', nullable: true, default: 2048 })
   maxBatchSize?: number;
 
   /**
-   * ベトルモデルかどうか(システム設定での識別用)
+   * Whether this is a vector model (for system identification)
    */
   @Column({ type: 'boolean', default: false })
   isVectorModel?: boolean;
 
   /**
-   * このモデルを有効にするかどうか
-   * ユーザーは使用しないモデルを無効にして、誤選択を防ぐことができます
+   * Whether to enable this model
+   * Users can disable models they don't use to prevent accidental selection
    */
   @Column({ type: 'boolean', default: true })
   isEnabled?: boolean;
 
   /**
-   * このモデルをデフォルトとして使用するかどうか
-   * 各タイプ(llm, embedding, rerank)ごとに1つのみデフォルトにできます
+   * Whether to use this model as default
+   * Only one default allowed per type (llm, embedding, rerank)
    */
   @Column({ type: 'boolean', default: false })
   isDefault?: boolean;
 
   /**
-   * モデルプロバイダー名(表示および識別用)
-   * : "OpenAI", "Google Gemini", "Custom"
+   * Model provider name (for display and identification)
+   * Example: "OpenAI", "Google Gemini", "Custom"
    */
   @Column({ type: 'text', nullable: true })
   providerName?: string;
 
-  // ==================== 既存のフィールド ====================
+  // ==================== Existing Fields ====================
 
   @Column({ type: 'text', nullable: true })
   userId: string;

+ 11 - 10
server/src/model-config/model-config.service.ts

@@ -7,6 +7,7 @@ import { UpdateModelConfigDto } from './dto/update-model-config.dto';
 import { GLOBAL_TENANT_ID } from '../common/constants';
 import { TenantService } from '../tenant/tenant.service';
 import { ModelType } from '../types';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class ModelConfigService {
@@ -15,6 +16,7 @@ export class ModelConfigService {
     private modelConfigRepository: Repository<ModelConfig>,
     @Inject(forwardRef(() => TenantService))
     private readonly tenantService: TenantService,
+    private i18nService: I18nService,
   ) { }
 
   async create(
@@ -50,7 +52,7 @@ export class ModelConfigService {
 
     if (!modelConfig) {
       throw new NotFoundException(
-        `ModelConfig with ID "${id}" not found.`,
+        this.i18nService.formatMessage('modelConfigNotFound', { id }),
       );
     }
     return modelConfig;
@@ -76,13 +78,13 @@ export class ModelConfigService {
 
     if (!modelConfig) {
       throw new NotFoundException(
-        `ModelConfig with ID "${id}" not found.`,
+        this.i18nService.formatMessage('modelConfigNotFound', { id }),
       );
     }
 
     // Only allow updating if it belongs to the tenant, or if it's a global admin (not fully implemented, so we check tenantId)
     if (modelConfig.tenantId && modelConfig.tenantId !== tenantId) {
-      throw new ForbiddenException('Cannot update models from another tenant');
+      throw new ForbiddenException(this.i18nService.getMessage('cannotUpdateOtherTenantModel'));
     }
 
     // Update the model
@@ -97,22 +99,21 @@ export class ModelConfigService {
     // Only allow removing if it exists and accessible in current tenant context
     const model = await this.findOne(id, userId, tenantId);
     if (model.tenantId && model.tenantId !== tenantId) {
-      throw new ForbiddenException('Cannot delete models from another tenant');
+      throw new ForbiddenException(this.i18nService.getMessage('cannotDeleteOtherTenantModel'));
     }
     const result = await this.modelConfigRepository.delete({ id });
     if (result.affected === 0) {
-      throw new NotFoundException(`ModelConfig with ID "${id}" not found.`);
+      throw new NotFoundException(this.i18nService.formatMessage('modelConfigNotFound', { id }));
     }
   }
 
   /**
-   * 指定されたモデルをデフォルトに設定
+   * Set the specified model as default
    */
   async setDefault(userId: string, tenantId: string, id: string): Promise<ModelConfig> {
     const modelConfig = await this.findOne(id, userId, tenantId);
 
-    // 同じタイプの他のモデルのデフォルトフラグをクリア (現在のテナント内またはglobal)
-    // 厳密には、現在のテナントのIsDefault設定といった方が正しいですが、シンプルにするため全体のIsDefaultを操作します
+    // Clear default flag for other models of the same type (within current tenant or global)
     await this.modelConfigRepository
       .createQueryBuilder()
       .update(ModelConfig)
@@ -129,8 +130,8 @@ export class ModelConfigService {
   }
 
   /**
-   * 指定されたタイプのデフォルトモデルを取得
-   * 厳密なルール:Index Chat Configで指定されたモデルのみを返し、なければエラーを投げる
+   * Get default model for specified type
+   * Strict rule: Only return models specified in Index Chat Config, throw error if not found
    */
   async findDefaultByType(tenantId: string, type: ModelType): Promise<ModelConfig> {
     const settings = await this.tenantService.getSettings(tenantId);

+ 8 - 8
server/src/note/note-category.service.ts

@@ -2,12 +2,14 @@ import { Injectable, NotFoundException } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Repository } from 'typeorm';
 import { NoteCategory } from './note-category.entity';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class NoteCategoryService {
     constructor(
         @InjectRepository(NoteCategory)
         private readonly categoryRepository: Repository<NoteCategory>,
+        private readonly i18nService: I18nService,
     ) { }
 
     async findAll(userId: string, tenantId: string): Promise<NoteCategory[]> {
@@ -24,10 +26,10 @@ export class NoteCategoryService {
                 where: { id: parentId, userId, tenantId }
             });
             if (!parent) {
-                throw new NotFoundException('Parent category not found');
+                throw new NotFoundException(this.i18nService.getMessage('parentCategoryNotFound'));
             }
             if (parent.level >= 3) {
-                throw new Error('Maximum category depth (3 levels) exceeded');
+                throw new Error(this.i18nService.getMessage('maxCategoryDepthExceeded'));
             }
             level = parent.level + 1;
         }
@@ -47,7 +49,7 @@ export class NoteCategoryService {
             where: { id, userId, tenantId },
         });
         if (!category) {
-            throw new NotFoundException('Category not found');
+            throw new NotFoundException(this.i18nService.getMessage('categoryNotFound'));
         }
 
         if (name !== undefined) {
@@ -62,14 +64,12 @@ export class NoteCategoryService {
                 const parent = await this.categoryRepository.findOne({
                     where: { id: parentId, userId, tenantId }
                 });
-                if (!parent) throw new NotFoundException('Parent category not found');
-                if (parent.level >= 3) throw new Error('Maximum category depth (3 levels) exceeded');
+                if (!parent) throw new NotFoundException(this.i18nService.getMessage('parentCategoryNotFound'));
+                if (parent.level >= 3) throw new Error(this.i18nService.getMessage('maxCategoryDepthExceeded'));
 
                 category.parentId = parentId;
                 category.level = parent.level + 1;
             }
-            // Note: In a real app we'd also need to update all children's levels recursively
-            // But for this requirement we'll assume shallow updates or handle edge cases
         }
 
         return this.categoryRepository.save(category);
@@ -78,7 +78,7 @@ export class NoteCategoryService {
     async remove(userId: string, tenantId: string, id: string): Promise<void> {
         const result = await this.categoryRepository.delete({ id, userId, tenantId });
         if (result.affected === 0) {
-            throw new NotFoundException('Category not found');
+            throw new NotFoundException(this.i18nService.getMessage('categoryNotFound'));
         }
     }
 }

+ 4 - 4
server/src/ocr/ocr.controller.ts

@@ -22,14 +22,14 @@ export class OcrController {
     @Post('recognize')
     @UseInterceptors(FileInterceptor('image'))
     async recognizeText(@UploadedFile() image: Express.Multer.File) {
-        console.log('OCR認識エンドポイントが呼び出されました');
+        console.log('OCR recognition endpoint called');
         if (!image) {
-            console.error('画像がアップロードされていません');
+            console.error('No image uploaded');
             throw new Error(this.i18n.getMessage('noImageUploaded'));
         }
-        console.log(`画像を受信しました。サイズ: ${image.size} bytes`);
+        console.log(`Received image. Size: ${image.size} bytes`);
         const text = await this.ocrService.extractTextFromImage(image.buffer);
-        console.log(`OCR抽出が完了しました。テキスト長: ${text.length}`);
+        console.log(`OCR extraction completed. Text length: ${text.length}`);
         return { text };
     }
 }

+ 7 - 7
server/src/ocr/ocr.service.ts

@@ -9,7 +9,7 @@ export class OcrService {
     constructor(private readonly i18n: I18nService) { }
 
     async extractTextFromImage(imageBuffer: Buffer): Promise<string> {
-        this.logger.log(`画像のOCR抽出を開始します (${imageBuffer.length} bytes)...`);
+        this.logger.log(`Starting OCR extraction from image (${imageBuffer.length} bytes)...`);
 
         // Create worker for this request to ensure stability
         let worker: any = null;
@@ -17,16 +17,16 @@ export class OcrService {
             worker = await createWorker('chi_sim+eng+jpn');
 
             const { data: { text } } = await worker.recognize(imageBuffer);
-            this.logger.log(`OCR抽出が完了しました。 ${text.length} 文字抽出されました。`);
+            this.logger.log(`OCR extraction completed. ${text.length} characters extracted.`);
 
             if (text.length === 0) {
-                this.logger.warn('OCRが空のテキストを返しました。');
+                this.logger.warn('OCR returned empty text.');
             }
 
             await worker.terminate();
             return text.trim();
         } catch (error) {
-            this.logger.error(`OCRテキスト抽出に失敗しました: ${error.message}`);
+            this.logger.error(`OCR text extraction failed: ${error.message}`);
             if (worker) {
                 try { await worker.terminate(); } catch (e) { }
             }
@@ -38,13 +38,13 @@ export class OcrService {
         text: string;
         confidence: number;
     }> {
-        this.logger.log(`信頼度付き画像のOCR抽出を開始します (${imageBuffer.length} bytes)...`);
+        this.logger.log(`Starting OCR extraction with confidence (${imageBuffer.length} bytes)...`);
 
         let worker: any = null;
         try {
             worker = await createWorker('chi_sim+eng+jpn');
             const { data } = await worker.recognize(imageBuffer);
-            this.logger.log(`OCR抽出が完了しました。信頼度: ${data.confidence}%`);
+            this.logger.log(`OCR extraction completed. Confidence: ${data.confidence}%`);
 
             await worker.terminate();
             return {
@@ -52,7 +52,7 @@ export class OcrService {
                 confidence: data.confidence,
             };
         } catch (error) {
-            this.logger.error(`OCRテキスト抽出に失敗しました: ${error.message}`);
+            this.logger.error(`OCR text extraction failed: ${error.message}`);
             if (worker) {
                 try { await worker.terminate(); } catch (e) { }
             }

+ 10 - 10
server/src/pdf2image/pdf2image.interface.ts

@@ -1,20 +1,20 @@
 /**
- * PDF 转图片接口定义
+ * PDF to Image Interface Definitions
  */
 
 export interface Pdf2ImageOptions {
-  density?: number;        // DPI 分辨率,默认 300
-  quality?: number;        // JPEG 质量 (1-100),默认 85
-  format?: 'jpeg' | 'png'; // 输出格式,默认 jpeg
-  outDir?: string;         // 输出目录,默认 ./temp
+  density?: number;        // DPI resolution, default 300
+  quality?: number;        // JPEG quality (1-100), default 85
+  format?: 'jpeg' | 'png'; // output format, default jpeg
+  outDir?: string;         // Output directory, default ./temp
 }
 
 export interface ImageInfo {
-  path: string;            // 图片文件路径
-  pageIndex: number;       // 页码(从 1 开始)
-  size: number;            // 文件大小(字节)
-  width?: number;          // 图片宽度
-  height?: number;         // 图片高度
+  path: string;            // Image file path
+  pageIndex: number;       // Page number (starting from 1)
+  size: number;            // File size (bytes)
+  width?: number;          // Image width
+  height?: number;         // Image height
 }
 
 export interface ConversionResult {

+ 26 - 22
server/src/pdf2image/pdf2image.service.ts

@@ -6,6 +6,7 @@ import { PDFDocument } from 'pdf-lib';
 import { exec } from 'child_process';
 import { promisify } from 'util';
 import { Pdf2ImageOptions, ImageInfo, ConversionResult } from './pdf2image.interface';
+import { I18nService } from '../i18n/i18n.service';
 
 const execAsync = promisify(exec);
 
@@ -14,13 +15,16 @@ export class Pdf2ImageService {
   private readonly logger = new Logger(Pdf2ImageService.name);
   private tempDir: string;
 
-  constructor(private configService: ConfigService) {
+  constructor(
+    private configService: ConfigService,
+    private i18nService: I18nService,
+  ) {
     this.tempDir = this.configService.get<string>('TEMP_DIR', './temp');
   }
 
   /**
-   * PDF を画像リストに変換します
-   * ImageMagick の convert コマンドを使用します
+   * Convert PDF to list of images
+   * Uses ImageMagick's convert command
    */
   async convertToImages(
     pdfPath: string,
@@ -33,14 +37,14 @@ export class Pdf2ImageService {
       outDir = this.tempDir,
     } = options;
 
-    // PDF ファイルの検証
+    // Validate PDF file
     try {
       await fs.access(pdfPath);
     } catch {
-      throw new Error(`PDF ファイルが存在しません: ${pdfPath}`);
+      throw new Error(`PDF file not found: ${pdfPath}`);
     }
 
-    // 出力ディレクトリの作成
+    // Create output directory
     const timestamp = Date.now();
     const outputDir = path.join(outDir, `pdf2img_${timestamp}`);
     await fs.mkdir(outputDir, { recursive: true });
@@ -49,30 +53,30 @@ export class Pdf2ImageService {
     this.logger.log(`Output directory: ${outputDir}`);
 
     try {
-      // PDF の総ページ数を取得 - pdfinfo の代わりに pdf-lib を使用
+      // Get total page count using pdf-lib instead of pdfinfo
       const pdfBytes = await fs.readFile(pdfPath);
       const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
       const totalPages = pdfDoc.getPageCount();
 
       if (totalPages === 0) {
-        throw new Error('PDF のページ数を取得できません');
+        throw new Error(this.i18nService.getMessage('pdfPageCountError'));
       }
 
-      this.logger.log(`📄 PDF 変換開始: ${path.basename(pdfPath)} (全 ${totalPages} ページ)`);
-      this.logger.log(`📁 出力ディレクトリ: ${outputDir}`);
-      this.logger.log(`⚙️  変換パラメータ: 密度=${density}dpi, 品質=${quality}%, 形式=${format}`);
+      this.logger.log(`Starting PDF conversion: ${path.basename(pdfPath)} (${totalPages} pages)`);
+      this.logger.log(`Output directory: ${outputDir}`);
+      this.logger.log(`Conversion parameters: density=${density}dpi, quality=${quality}%, format=${format}`);
 
-      // Python スクリプトを使用して変換
+      // Convert using Python script
       const zoom = (density / 72).toFixed(2);
       const pythonScript = path.join(process.cwd(), 'pdf_to_images.py');
       const cmd = `python "${pythonScript}" "${pdfPath}" "${outputDir}" ${zoom} ${quality}`;
 
-      this.logger.log(`変換コマンドを実行中: ${cmd}`);
+      this.logger.log(`Executing conversion command: ${cmd}`);
       const { stdout } = await execAsync(cmd);
       const result = JSON.parse(stdout);
 
       if (!result.success) {
-        throw new Error(`Python での変換に失敗しました: ${result.error}`);
+        throw new Error(`Python conversion failed: ${result.error}`);
       }
 
       const images: ImageInfo[] = result.images;
@@ -80,7 +84,7 @@ export class Pdf2ImageService {
       const failedCount = totalPages - successCount;
 
       this.logger.log(
-        `🎉 PDF 変換完了! ✅ 成功: ${successCount} ページ, ❌ 失敗: ${failedCount} ページ, 📊 総ページ数: ${totalPages}`
+        `🎉 PDF conversion completed! ✅ Success: ${successCount} pages, ❌ Failed: ${failedCount} pages, 📊 Total pages: ${totalPages}`
       );
 
       return {
@@ -90,14 +94,14 @@ export class Pdf2ImageService {
         failedCount,
       };
     } catch (error) {
-      // 一時ディレクトリのクリーンアップ
+      // Cleanup temp directory
       await this.cleanupDirectory(outputDir);
-      throw new Error(`PDF から画像への変換に失敗しました: ${error.message}`);
+      throw new Error(`PDF to image conversion failed: ${error.message}`);
     }
   }
 
   /**
-   * 複数の PDF を一括変換
+   * Batch convert multiple PDFs
    */
   async batchConvert(pdfPaths: string[], options?: Pdf2ImageOptions): Promise<ConversionResult[]> {
     const results: ConversionResult[] = [];
@@ -114,7 +118,7 @@ export class Pdf2ImageService {
   }
 
   /**
-   * 画像ファイルのクリーンアップ
+   * Cleanup image files
    */
   async cleanupImages(images: ImageInfo[]): Promise<void> {
     for (const image of images) {
@@ -126,7 +130,7 @@ export class Pdf2ImageService {
       }
     }
 
-    // 空のディレクトリのクリーンアップを試行
+    // Try to cleanup empty directory
     if (images.length > 0) {
       const dir = path.dirname(images[0].path);
       await this.cleanupDirectory(dir);
@@ -134,7 +138,7 @@ export class Pdf2ImageService {
   }
 
   /**
-   * ディレクトリのクリーンアップ
+   * Cleanup directory
    */
   async cleanupDirectory(dir: string): Promise<void> {
     try {
@@ -149,7 +153,7 @@ export class Pdf2ImageService {
   }
 
   /**
-   * 画像品質が妥当か確認
+   * Check if image quality is acceptable
    */
   isImageQualityGood(imageInfo: ImageInfo, minSizeKB: number = 10): boolean {
     const sizeKB = imageInfo.size / 1024;

+ 5 - 6
server/src/podcasts/podcast.service.ts

@@ -9,6 +9,7 @@ import * as path from 'path';
 import { v4 as uuidv4 } from 'uuid';
 import { KnowledgeGroup } from '../knowledge-group/knowledge-group.entity';
 import { ChatService } from '../chat/chat.service';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class PodcastService {
@@ -24,6 +25,7 @@ export class PodcastService {
         private groupRepository: Repository<KnowledgeGroup>,
         private configService: ConfigService,
         private chatService: ChatService, // Reusing ChatService to generate script
+        private i18nService: I18nService,
     ) {
         // this.tts = new EdgeTTS();
         this.outputDir = path.join(process.cwd(), 'uploads', 'podcasts');
@@ -33,7 +35,7 @@ export class PodcastService {
     async create(userId: string, createDto: any): Promise<PodcastEpisode> {
         this.logger.log(`Creating podcast with DTO: ${JSON.stringify(createDto)}`);
         if (!userId) {
-            throw new Error('User ID is required to create a podcast');
+            throw new Error(this.i18nService.getMessage('userIdRequired'));
         }
 
         const episode = this.podcastRepository.create({
@@ -66,7 +68,7 @@ export class PodcastService {
 
     async findOne(userId: string, id: string): Promise<PodcastEpisode> {
         const episode = await this.podcastRepository.findOne({ where: { id, userId } });
-        if (!episode) throw new NotFoundException(`Podcast ${id} not found`);
+        if (!episode) throw new NotFoundException(this.i18nService.formatMessage('podcastNotFound', { id }));
         return episode;
     }
 
@@ -185,10 +187,7 @@ export class PodcastService {
                 return JSON.parse(jsonString);
             } catch (e) {
                 this.logger.error('Failed to parse podcast script JSON:', rawContent);
-                // Fallback parsing if JSON fails? Or just throw.
-                // Simple fallback: Split by newlines and try to guess speaker? 
-                // For now, throw to see errors.
-                throw new Error('Script generation failed to produce valid JSON');
+                throw new Error(this.i18nService.getMessage('scriptGenerationFailed'));
             }
         } catch (error) {
             this.logger.error('Failed to generate script:', error);

+ 32 - 32
server/src/rag/rag.service.ts

@@ -16,7 +16,7 @@ export interface RagSearchResult {
   score: number;
   chunkIndex: number;
   fileId?: string;
-  originalScore?: number; // Rerank前のスコア(デバッグ用)
+  originalScore?: number; // Original score before reranking (for debugging)
   metadata?: any;
 }
 
@@ -41,29 +41,29 @@ export class RagService {
     this.defaultDimensions = parseInt(
       this.configService.get<string>('DEFAULT_VECTOR_DIMENSIONS', '2560'),
     );
-    this.logger.log(`RAG サービスのデフォルトベクトル次元数: ${this.defaultDimensions}`);
+    this.logger.log(`RAG service default vector dimensions: ${this.defaultDimensions}`);
   }
 
   async searchKnowledge(
     query: string,
     userId: string,
     topK: number = 5,
-    vectorSimilarityThreshold: number = 0.3, // ベクトル検索のしきい値
+    vectorSimilarityThreshold: number = 0.3, // Vector search similarity threshold
     embeddingModelId?: string,
     enableFullTextSearch: boolean = false,
     enableRerank: boolean = false,
     rerankModelId?: string,
     selectedGroups?: string[],
     effectiveFileIds?: string[],
-    rerankSimilarityThreshold: number = 0.5, // Rerankのしきい値(デフォルト0.5)
+    rerankSimilarityThreshold: number = 0.5, // Rerank similarity threshold (default 0.5)
     tenantId?: string, // New
     enableQueryExpansion?: boolean,
     enableHyDE?: boolean,
   ): Promise<RagSearchResult[]> {
-    // 1. 組織設定の取得
+    // 1. Get organization settings
     const globalSettings = await this.tenantService.getSettings(tenantId || 'default');
 
-    // パラメータが明示的に渡されていない場合はグローバル設定を使用
+    // Use global settings if parameters are not explicitly provided
     const effectiveTopK = topK || globalSettings?.topK || 5;
     const effectiveVectorThreshold = vectorSimilarityThreshold !== undefined ? vectorSimilarityThreshold : (globalSettings?.similarityThreshold || 0.3);
     const effectiveRerankThreshold = rerankSimilarityThreshold !== undefined ? rerankSimilarityThreshold : (globalSettings?.rerankSimilarityThreshold || 0.5);
@@ -80,25 +80,25 @@ export class RagService {
     );
 
     try {
-      // 1. クエリの準備(拡張または HyDE)
+      // 1. Prepare query (expansion or HyDE)
       let queriesToSearch = [query];
 
       if (effectiveEnableHyDE) {
         const hydeDoc = await this.generateHyDE(query, userId);
-        queriesToSearch = [hydeDoc]; // HyDE の場合は仮想ドキュメントをクエリとして使用
+        queriesToSearch = [hydeDoc]; // Use virtual document as query for HyDE
       } else if (effectiveEnableQueryExpansion) {
         const expanded = await this.expandQuery(query, userId);
         queriesToSearch = [...new Set([query, ...expanded])];
       }
 
-      // 埋め込みモデルIDが提供されているか確認
+      // Check if embedding model ID is provided
       if (!effectiveEmbeddingId) {
-        throw new Error('埋め込みモデルIDが提供されていません');
+        throw new Error(this.i18nService.getMessage('embeddingModelIdNotProvided'));
       }
 
-      // 2. 複数のクエリに対して並列検索
+      // 2. Parallel search for multiple queries
       const searchTasks = queriesToSearch.map(async (searchQuery) => {
-        // クエリベクトルの取得
+        // Get query vector
         const queryEmbedding = await this.embeddingService.getEmbeddings(
           [searchQuery],
           userId,
@@ -106,7 +106,7 @@ export class RagService {
         );
         const queryVector = queryEmbedding[0];
 
-        // 設定に基づいた検索戦略の選択
+        // Select search strategy based on settings
         let results;
         if (effectiveEnableFullText) {
           results = await this.elasticsearchService.hybridSearch(
@@ -138,19 +138,19 @@ export class RagService {
       const allResultsRaw = await Promise.all(searchTasks);
       let searchResults = this.deduplicateResults(allResultsRaw.flat());
 
-      // 初回の類似度フィルタリング
+      // Initial similarity filtering
       const initialCount = searchResults.length;
 
-      // ログ出力
+      // Log output
       searchResults.forEach((r, idx) => {
         this.logger.log(`Hit ${idx}: score=${r.score.toFixed(4)}, fileName=${r.fileName}`);
       });
 
-      // 閾値フィルタリングを適用
+      // Apply threshold filtering
       searchResults = searchResults.filter(r => r.score >= effectiveVectorThreshold);
       this.logger.log(`Initial hits: ${initialCount} -> filtered by vectorThreshold: ${searchResults.length}`);
 
-      // 3. リランク (Rerank)
+      // 3. Rerank
       let finalResults = searchResults;
 
       if (effectiveEnableRerank && effectiveRerankId && searchResults.length > 0) {
@@ -161,33 +161,33 @@ export class RagService {
             docs,
             userId,
             effectiveRerankId,
-            effectiveTopK * 2 // 少し多めに残す
+            effectiveTopK * 2 // Keep a bit more results
           );
 
           finalResults = rerankedIndices.map(r => {
             const originalItem = searchResults[r.index];
             return {
               ...originalItem,
-              score: r.score, // Rerank スコア
-              originalScore: originalItem.score // 元のスコア
+              score: r.score, // Rerank score
+              originalScore: originalItem.score // Original score
             };
           });
 
-          // Rerank後のフィルタリング
+          // Filter after reranking
           const beforeRerankFilter = finalResults.length;
           finalResults = finalResults.filter(r => r.score >= effectiveRerankThreshold);
           this.logger.log(`After rerank: ${beforeRerankFilter} -> filtered by rerankThreshold: ${finalResults.length}`);
 
         } catch (error) {
           this.logger.warn(`Rerank failed, falling back to filtered vector search: ${error.message}`);
-          // 失敗した場合はベクトル検索の結果をそのまま使う
+          // Fall back to filtered vector search results if rerank fails
         }
       }
 
-      // 最終的な件数制限
+      // Final result count limit
       finalResults = finalResults.slice(0, effectiveTopK);
 
-      // 4. RAG 結果形式に変換
+      // 4. Convert to RAG result format
       const ragResults: RagSearchResult[] = finalResults.map((result) => ({
         content: result.content,
         fileName: result.fileName,
@@ -212,12 +212,12 @@ export class RagService {
   ): string {
     const lang = language || 'ja';
 
-    // コンテキストの構築
+    // Build context
     let context = '';
     if (searchResults.length === 0) {
       context = this.i18nService.getMessage('ragNoDocumentFound', lang);
     } else {
-      // ファイルごとにグループ化
+      // Group by file
       const fileGroups = new Map<string, RagSearchResult[]>();
       searchResults.forEach((result) => {
         if (!fileGroups.has(result.fileName)) {
@@ -226,7 +226,7 @@ export class RagService {
         fileGroups.get(result.fileName)!.push(result);
       });
 
-      // コンテキスト文字列を構築
+      // Build context string
       const contextParts: string[] = [];
       fileGroups.forEach((chunks, fileName) => {
         contextParts.push(this.i18nService.formatMessage('ragSource', { fileName }, lang));
@@ -275,7 +275,7 @@ ${answerHeader}`;
   }
 
   /**
-   * 検索結果の重複排除
+   * Deduplicate search results
    */
   private deduplicateResults(results: any[]): any[] {
     const unique = new Map<string, any>();
@@ -289,7 +289,7 @@ ${answerHeader}`;
   }
 
   /**
-   * クエリを拡張してバリエーションを生成
+   * Expand query to generate variations
    */
   async expandQuery(query: string, userId: string, tenantId?: string): Promise<string[]> {
     try {
@@ -307,7 +307,7 @@ ${answerHeader}`;
         .split('\n')
         .map(q => q.trim())
         .filter(q => q.length > 0)
-        .slice(0, 3); // 最大3つに制限
+        .slice(0, 3); // Limit to maximum 3
 
       this.logger.log(`Query expanded: "${query}" -> [${expandedQueries.join(', ')}]`);
       return expandedQueries.length > 0 ? expandedQueries : [query];
@@ -318,7 +318,7 @@ ${answerHeader}`;
   }
 
   /**
-   * 仮想的なドキュメント(HyDE)を生成
+   * Generate hypothetical document (HyDE)
    */
   async generateHyDE(query: string, userId: string, tenantId?: string): Promise<string> {
     try {
@@ -341,7 +341,7 @@ ${answerHeader}`;
   }
 
   /**
-   * 内部タスク用の LLM インスタンスを取得
+   * Get LLM instance for internal tasks
    */
   private async getInternalLlm(userId: string, tenantId: string): Promise<ChatOpenAI | null> {
     try {

+ 11 - 11
server/src/rag/rerank.service.ts

@@ -20,12 +20,12 @@ export class RerankService {
     ) { }
 
     /**
-     * リランクの実行
-     * @param query ユーザーのクエリ
-     * @param documents 候補ドキュメントリスト
-     * @param userId ユーザーID
-     * @param rerankModelId 選択された Rerank モデル設定ID
-     * @param topN 返す結果の数 (上位 N 個)
+     * Execute reranking
+     * @param query User query
+     * @param documents Candidate document list
+     * @param userId User ID
+     * @param rerankModelId Selected rerank model config ID
+     * @param topN Number of results to return (top N)
      */
     async rerank(
         query: string,
@@ -41,7 +41,7 @@ export class RerankService {
 
         let modelConfig;
         try {
-            // 1. モデル設定の取得
+            // 1. Get model config
             modelConfig = await this.modelConfigService.findOne(rerankModelId, userId, tenantId || 'default');
 
             if (!modelConfig || modelConfig.type !== ModelType.RERANK) {
@@ -56,9 +56,9 @@ export class RerankService {
 
             this.logger.log(`Reranking ${documents.length} docs with model ${modelName} at ${baseUrl}`);
 
-            // 2. API リクエストの構築 (OpenAI/SiliconFlow 互換 Rerank API)
-            // 注: 標準の OpenAI API には /rerank はありませんが、SiliconFlow/Jina/Cohere は同様の構造を使用しています
-            // SiliconFlow 形式: POST /v1/rerank { model, query, documents, top_n }
+            // 2. Build API request (OpenAI/SiliconFlow compatible Rerank API)
+            // Note: Standard OpenAI API does not have /rerank, but SiliconFlow/Jina/Cohere use similar structure
+            // SiliconFlow format: POST /v1/rerank { model, query, documents, top_n }
 
             const endpoint = baseUrl.replace(/\/+$/, '');
 
@@ -83,7 +83,7 @@ export class RerankService {
                 }
             );
 
-            // 3. レスポンスの解析
+            // 3. Parse response
             // Expected response format (SiliconFlow/Cohere):
             // { results: [ { index: 0, relevance_score: 0.98 }, ... ] }
 

+ 6 - 2
server/src/search-history/search-history.controller.ts

@@ -11,11 +11,15 @@ import {
 } from '@nestjs/common';
 import { CombinedAuthGuard } from '../auth/combined-auth.guard';
 import { SearchHistoryService } from './search-history.service';
+import { I18nService } from '../i18n/i18n.service';
 
 @Controller('search-history')
 @UseGuards(CombinedAuthGuard)
 export class SearchHistoryController {
-  constructor(private readonly searchHistoryService: SearchHistoryService) { }
+  constructor(
+    private readonly searchHistoryService: SearchHistoryService,
+    private readonly i18nService: I18nService,
+  ) { }
 
   @Get()
   async findAll(
@@ -51,6 +55,6 @@ export class SearchHistoryController {
   @Delete(':id')
   async remove(@Param('id') id: string, @Request() req) {
     await this.searchHistoryService.remove(id, req.user.id, req.user.tenantId);
-    return { message: '对话历史删除成功' };
+    return { message: this.i18nService.getMessage('searchHistoryDeleted') };
   }
 }

+ 1 - 1
server/src/search-history/search-history.service.ts

@@ -147,7 +147,7 @@ export class SearchHistoryService {
 
     const savedMessage = await this.chatMessageRepository.save(message);
 
-    // 履歴レコードの更新時間を更新
+    // Update history record update time
     await this.searchHistoryRepository.update(historyId, {
       updatedAt: new Date(),
     });

+ 3 - 1
server/src/tenant/tenant.service.ts

@@ -9,6 +9,7 @@ import { Repository } from 'typeorm';
 import { Tenant } from './tenant.entity';
 import { TenantSetting } from './tenant-setting.entity';
 import { TenantMember } from './tenant-member.entity';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class TenantService {
@@ -21,6 +22,7 @@ export class TenantService {
         private readonly tenantSettingRepository: Repository<TenantSetting>,
         @InjectRepository(TenantMember)
         private readonly tenantMemberRepository: Repository<TenantMember>,
+        private readonly i18nService: I18nService,
     ) { }
 
     async findAll(page?: number, limit?: number): Promise<{ data: Tenant[]; total: number } | Tenant[]> {
@@ -42,7 +44,7 @@ export class TenantService {
 
     async findById(id: string): Promise<Tenant> {
         const tenant = await this.tenantRepository.findOneBy({ id });
-        if (!tenant) throw new NotFoundException(`Tenant ${id} not found`);
+        if (!tenant) throw new NotFoundException(this.i18nService.getMessage('tenantNotFound'));
         return tenant;
     }
 

+ 6 - 2
server/src/tika/tika.service.ts

@@ -1,16 +1,20 @@
 import { Injectable, Logger } from '@nestjs/common';
 import { ConfigService } from '@nestjs/config';
 import * as fs from 'fs';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class TikaService {
   private readonly logger = new Logger(TikaService.name);
   private readonly tikaHost: string;
 
-  constructor(private configService: ConfigService) {
+  constructor(
+    private configService: ConfigService,
+    private i18nService: I18nService,
+  ) {
     const tikaHost = this.configService.get<string>('TIKA_HOST');
     if (!tikaHost) {
-      throw new Error('TIKA_HOST environment variable is required but not set');
+      throw new Error(this.i18nService.getMessage('tikaHostRequired'));
     }
     this.tikaHost = tikaHost;
   }

+ 9 - 9
server/src/upload/upload.controller.ts

@@ -34,7 +34,7 @@ export interface UploadConfigDto {
   chunkSize?: string;
   chunkOverlap?: string;
   embeddingModelId?: string;
-  mode?: 'fast' | 'precise'; // 処理モード
+  mode?: 'fast' | 'precise'; // Processing mode
   groupIds?: string; // JSON string of group IDs
 }
 
@@ -54,7 +54,7 @@ export class UploadController {
   @UseInterceptors(
     FileInterceptor('file', {
       fileFilter: (req, file, cb) => {
-        // 画像MIMEタイプまたは拡張子によるチェック
+        // Check by image MIME type or extension
         const isAllowed = IMAGE_MIME_TYPES.includes(file.mimetype) ||
           isAllowedByExtension(file.originalname);
 
@@ -80,7 +80,7 @@ export class UploadController {
       throw new BadRequestException(this.i18nService.getMessage('uploadNoFile'));
     }
 
-    // ファイルサイズの検証(フロントエンド制限 + バックエンド検証
+    // Validate file size(frontend limit + backend validation
     const maxFileSize = parseInt(
       process.env.MAX_FILE_SIZE || String(MAX_FILE_SIZE),
     ); // 100MB
@@ -90,18 +90,18 @@ export class UploadController {
       );
     }
 
-    // 埋め込みモデル設定の検証
+    // Validate embedding model config
     if (!config.embeddingModelId) {
       throw new BadRequestException(this.i18nService.getMessage('uploadModelRequired'));
     }
 
     this.logger.log(
-      `ユーザー ${req.user.id} がファイルをアップロードしました: ${file.originalname} (${this.formatBytes(file.size)})`,
+      `User ${req.user.id} uploaded file: ${file.originalname} (${this.formatBytes(file.size)})`,
     );
 
     const fileInfo = await this.uploadService.processUploadedFile(file);
 
-    // 設定パラメータを解析し、安全なデフォルト値を設定
+    // Parse config parameters and set safe default values
     const indexingConfig = {
       chunkSize: config.chunkSize ? parseInt(config.chunkSize) : DEFAULT_CHUNK_SIZE,
       chunkOverlap: config.chunkOverlap ? parseInt(config.chunkOverlap) : DEFAULT_CHUNK_OVERLAP,
@@ -109,7 +109,7 @@ export class UploadController {
       groupIds: config.groupIds ? JSON.parse(config.groupIds) : [],
     };
 
-    // オーバーラップサイズがチャンクサイズの50%を超えないようにする
+    // Ensure overlap <= 50% of chunk size
     if (indexingConfig.chunkOverlap > indexingConfig.chunkSize * DEFAULT_MAX_OVERLAP_RATIO) {
       indexingConfig.chunkOverlap = Math.floor(indexingConfig.chunkSize * DEFAULT_MAX_OVERLAP_RATIO);
       this.logger.warn(
@@ -117,7 +117,7 @@ export class UploadController {
       );
     }
 
-    // データベースに保存し、インデックスプロセスをトリガー(非同期
+    // Save to database and trigger indexing(async
     const kb = await this.knowledgeBaseService.createAndIndex(
       fileInfo,
       req.user.id,
@@ -136,7 +136,7 @@ export class UploadController {
       status: kb.status,
       mode: config.mode || 'fast',
       config: indexingConfig,
-      estimatedChunks: Math.ceil(file.size / (indexingConfig.chunkSize * 4)), // 推定チャンク数
+      estimatedChunks: Math.ceil(file.size / (indexingConfig.chunkSize * 4)), // Estimated chunk count
     };
   }
 

+ 5 - 5
server/src/upload/upload.module.ts

@@ -19,14 +19,14 @@ import { UserModule } from '../user/user.module';
         const uploadPath = configService.get<string>(
           'UPLOAD_FILE_PATH',
           './uploads',
-        ); // 環境変数からアップロードパスを取得し、ない場合はデフォルトとして './uploads' を使用します
+        ); // Get upload path from env varor use default './uploads' use
 
-        // アップロードディレクトリが存在することを確認
+        // Ensure upload directory exists
         if (!fs.existsSync(uploadPath)) {
           fs.mkdirSync(uploadPath, { recursive: true });
         }
 
-        // 環境変数から最大ファイルサイズ制限を取得、デフォルトは 100MB
+        // Get max file size from env var, default 100MB
         const maxFileSize = parseInt(
           configService.get<string>('MAX_FILE_SIZE', '104857600'), // 100MB in bytes
         );
@@ -43,7 +43,7 @@ import { UserModule } from '../user/user.module';
               cb(null, fullPath);
             },
             filename: (req, file, cb) => {
-              // 中国語ファイル名の文字化け問題を解決
+              // Fix Chinese filename garbling
               file.originalname = Buffer.from(
                 file.originalname,
                 'latin1',
@@ -57,7 +57,7 @@ import { UserModule } from '../user/user.module';
             },
           }),
           limits: {
-            fileSize: maxFileSize, // ファイルサイズの制限
+            fileSize: maxFileSize, // File size limit
           },
         };
       },

+ 6 - 6
server/src/upload/upload.service.ts

@@ -3,18 +3,18 @@ import { Injectable } from '@nestjs/common';
 @Injectable()
 export class UploadService {
   async processUploadedFile(file: Express.Multer.File) {
-    // ここにさらに業務ロジックを追加できます。例:
-    // - ファイル情報をデータベースに保存
-    // - ファイルを処理するために他のサービスを呼び出す(Tika テキスト抽出、Elasticsearch インデックス作成など)
-    // - ファイル形式の検証や内容分析を行う
+    // Add more business logic here. Example:
+    // - Save file info to database
+    // - Call other services to process file (Tika text extraction, ES indexing etc.)
+    // - Validate file format or analyze content
 
-    // 現時点では、ファイルの基本情報のみを返します
+    // Currently only return basic file info
     return {
       filename: file.filename,
       originalname: file.originalname,
       size: file.size,
       mimetype: file.mimetype,
-      path: file.path, // Multer がファイルを保存した後、ファイルオブジェクトの path プロパティにフルパスが含まれます
+      path: file.path, // After Multer saves file, full path is in file.path
     };
   }
 }

+ 4 - 4
server/src/user/user.controller.ts

@@ -179,7 +179,7 @@ export class UserController {
       throw new ForbiddenException(this.i18nService.getErrorMessage('adminOnlyUpdateUser'));
     }
 
-    // 更新するユーザー情報を取得
+    // Get user info to update
     const userToUpdate = await this.userService.findOneById(id);
     if (!userToUpdate) {
       throw new NotFoundException(this.i18nService.getErrorMessage('userNotFound'));
@@ -215,12 +215,12 @@ export class UserController {
       throw new ForbiddenException(this.i18nService.getErrorMessage('adminOnlyDeleteUser'));
     }
 
-    // 管理者が自身を削除するのを防止
+    // Prevent admin from deleting themselves
     if (req.user.id === id) {
       throw new BadRequestException(this.i18nService.getErrorMessage('cannotDeleteSelf'));
     }
 
-    // 削除するユーザー情報を取得
+    // Get user info to delete
     const userToDelete = await this.userService.findOneById(id);
     if (!userToDelete) {
       throw new NotFoundException(this.i18nService.getErrorMessage('userNotFound'));
@@ -230,7 +230,7 @@ export class UserController {
       throw new ForbiddenException('Cannot delete users outside your tenant');
     }
 
-    // ビルトインadminアカウントの削除を阻止
+    // Block deletion of built-in admin account
     if (userToDelete.username === 'admin') {
       throw new ForbiddenException(this.i18nService.getErrorMessage('cannotDeleteBuiltinAdmin'));
     }

+ 1 - 1
server/src/user/user.entity.ts

@@ -50,7 +50,7 @@ export class User {
   @OneToMany(() => ApiKey, (apiKey) => apiKey.user)
   apiKeys: ApiKey[];
 
-  // クォータ管理フィールド
+  // Quota management field
   @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
   monthlyCost: number;
 

+ 6 - 6
server/src/user/user.service.ts

@@ -250,13 +250,13 @@ export class UserService implements OnModuleInit {
       throw new NotFoundException(this.i18nService.getMessage('userNotFound'));
     }
 
-    // パスワードの更新が必要な場合は、まずハッシュ化する
+    // Hash password first if update needed
     if (updateData.password) {
       const hashedPassword = await bcrypt.hash(updateData.password, 10);
       updateData.password = hashedPassword;
     }
 
-    // ユーザー名 "admin" のユーザーに対するいかなる変更も阻止
+    // Block any changes to user "admin"
     if (user.username === 'admin') {
       throw new ForbiddenException(this.i18nService.getMessage('cannotModifyBuiltinAdmin'));
     }
@@ -286,7 +286,7 @@ export class UserService implements OnModuleInit {
       throw new NotFoundException(this.i18nService.getMessage('userNotFound'));
     }
 
-    // ユーザー名 "admin" のユーザーの削除を阻止
+    // Block deletion of user "admin"
     if (user.username === 'admin') {
       throw new ForbiddenException(this.i18nService.getMessage('cannotDeleteBuiltinAdmin'));
     }
@@ -314,9 +314,9 @@ export class UserService implements OnModuleInit {
         isAdmin: true,
       });
 
-      console.log('\n=== 管理者アカウントが作成されました ===');
-      console.log('ユーザー名: admin');
-      console.log('パスワード:', randomPassword);
+      console.log('\n=== Admin account created ===');
+      console.log('Username: admin');
+      console.log('Password:', randomPassword);
       console.log('========================================\n');
     }
   }

+ 41 - 41
server/src/vision-pipeline/cost-control.service.ts

@@ -1,6 +1,6 @@
 /**
- * コスト制御およびクォータ管理サービス
- * Vision Pipeline の API 呼び出しコストを管理するために使用されます
+ * Cost control and quota management service
+ * Used to manage API call costs for Vision Pipeline
  */
 
 import { Injectable, Logger } from '@nestjs/common';
@@ -11,16 +11,16 @@ import { User } from '../user/user.entity';
 
 export interface UserQuota {
   userId: string;
-  monthlyCost: number;      // 今月の使用済みコスト
-  maxCost: number;          // 月間最大コスト
-  remaining: number;        // 残りコスト
-  lastReset: Date;          // 最終リセット時間
+  monthlyCost: number;      // Current month used cost
+  maxCost: number;          // Monthly max cost
+  remaining: number;        // Remaining cost
+  lastReset: Date;          // Last reset time
 }
 
 export interface CostEstimate {
-  estimatedCost: number;    // 推定コスト
-  estimatedTime: number;    // 推定時間(秒)
-  pageBreakdown: {          // ページごとの明細
+  estimatedCost: number;    // Estimated cost
+  estimatedTime: number;    // Estimated time(seconds)
+  pageBreakdown: {          // Per-page breakdown
     pageIndex: number;
     cost: number;
   }[];
@@ -29,8 +29,8 @@ export interface CostEstimate {
 @Injectable()
 export class CostControlService {
   private readonly logger = new Logger(CostControlService.name);
-  private readonly COST_PER_PAGE = 0.01; // 1ページあたりのコスト(USD)
-  private readonly DEFAULT_MONTHLY_LIMIT = 100; // デフォルトの月間制限(USD)
+  private readonly COST_PER_PAGE = 0.01; // Cost per page(USD)
+  private readonly DEFAULT_MONTHLY_LIMIT = 100; // Default monthly limit(USD)
 
   constructor(
     private configService: ConfigService,
@@ -39,10 +39,10 @@ export class CostControlService {
   ) { }
 
   /**
-   * 処理コストの推定
+   * Estimate processing cost
    */
   estimateCost(pageCount: number, quality: 'low' | 'medium' | 'high' = 'medium'): CostEstimate {
-    // 品質に基づいてコスト係数を調整
+    // Adjust cost coefficient based on quality
     const qualityMultiplier = {
       low: 0.5,
       medium: 1.0,
@@ -50,7 +50,7 @@ export class CostControlService {
     };
 
     const baseCost = pageCount * this.COST_PER_PAGE * qualityMultiplier[quality];
-    const estimatedTime = pageCount * 3; // 1ページあたり約 3 秒
+    const estimatedTime = pageCount * 3; // // Approximately 3 seconds
 
     const pageBreakdown = Array.from({ length: pageCount }, (_, i) => ({
       pageIndex: i + 1,
@@ -65,7 +65,7 @@ export class CostControlService {
   }
 
   /**
-   * ユーザーのクォータをチェック
+   * Check user quota
    */
   async checkQuota(userId: string, estimatedCost: number): Promise<{
     allowed: boolean;
@@ -74,17 +74,17 @@ export class CostControlService {
   }> {
     const quota = await this.getUserQuota(userId);
 
-    // 月間リセットのチェック
+    // Check monthly reset
     this.checkAndResetMonthlyQuota(quota);
 
     if (quota.remaining < estimatedCost) {
       this.logger.warn(
-        `ユーザー ${userId} のクォータが不足しています: 残り $${quota.remaining.toFixed(2)}, 必要 $${estimatedCost.toFixed(2)}`,
+        `User ${userId} quota insufficient: remaining $${quota.remaining.toFixed(2)}, required $${estimatedCost.toFixed(2)}`,
       );
       return {
         allowed: false,
         quota,
-        reason: `クォータ不足: 残り $${quota.remaining.toFixed(2)}, 必要 $${estimatedCost.toFixed(2)}`,
+        reason: `Insufficient quota: remaining $${quota.remaining.toFixed(2)}, required $${estimatedCost.toFixed(2)}`,
       };
     }
 
@@ -95,7 +95,7 @@ export class CostControlService {
   }
 
   /**
-   * クォータの差し引き
+   * Deduct from quota
    */
   async deductQuota(userId: string, actualCost: number): Promise<void> {
     const quota = await this.getUserQuota(userId);
@@ -107,21 +107,21 @@ export class CostControlService {
     });
 
     this.logger.log(
-      `ユーザー ${userId} のクォータから $${actualCost.toFixed(2)} を差し引きました。残り $${quota.remaining.toFixed(2)}`,
+      `Deducted $${actualCost.toFixed(2)} from user ${userId} quota. Remaining: $${quota.remaining.toFixed(2)}`,
     );
   }
 
   /**
-   * ユーザーのクォータを取得
+   * Get user quota
    */
   async getUserQuota(userId: string): Promise<UserQuota> {
     const user = await this.userRepository.findOne({ where: { id: userId } });
 
     if (!user) {
-      throw new Error(`ユーザー ${userId} は存在しません`);
+      throw new Error(`User ${userId} does not exist`);
     }
 
-    // ユーザーにクォータ情報がない場合はデフォルト値を使用
+    // Use default if user has no quota info
     const monthlyCost = user.monthlyCost || 0;
     const maxCost = user.maxCost || this.DEFAULT_MONTHLY_LIMIT;
     const lastReset = user.lastQuotaReset || new Date();
@@ -136,25 +136,25 @@ export class CostControlService {
   }
 
   /**
-   * 月間クォータのチェックとリセット
+   * Check and reset monthly quota
    */
   private checkAndResetMonthlyQuota(quota: UserQuota): void {
     const now = new Date();
     const lastReset = quota.lastReset;
 
-    // 月を跨いでいるかチェック
+    // Check if crossed month
     if (
       now.getMonth() !== lastReset.getMonth() ||
       now.getFullYear() !== lastReset.getFullYear()
     ) {
-      this.logger.log(`ユーザー ${quota.userId} の月間クォータをリセットしました`);
+      this.logger.log(`Reset monthly quota for user ${quota.userId}`);
 
-      // クォータをリセット
+      // Reset quota
       quota.monthlyCost = 0;
       quota.remaining = quota.maxCost;
       quota.lastReset = now;
 
-      // データベースを更新
+      // Update database
       this.userRepository.update(quota.userId, {
         monthlyCost: 0,
         lastQuotaReset: now,
@@ -163,15 +163,15 @@ export class CostControlService {
   }
 
   /**
-   * ユーザーのクォータ制限を設定
+   * Set user quota limit
    */
   async setQuotaLimit(userId: string, maxCost: number): Promise<void> {
     await this.userRepository.update(userId, { maxCost });
-    this.logger.log(`ユーザー ${userId} のクォータ制限を $${maxCost} に設定しました`);
+    this.logger.log(`Set quota limit to $${maxCost} for user ${userId}`);
   }
 
   /**
-   * コストレポートの取得
+   * Get cost report
    */
   async getCostReport(userId: string, days: number = 30): Promise<{
     totalCost: number;
@@ -180,13 +180,13 @@ export class CostControlService {
       totalPages: number;
       avgCostPerPage: number;
     };
-    quotaUsage: number; // パーセンテージ
+    quotaUsage: number; // Percentage
   }> {
     const quota = await this.getUserQuota(userId);
     const usagePercent = (quota.monthlyCost / quota.maxCost) * 100;
 
-    // ここで履歴レコードを照会できます(実装されている場合)
-    // 暫定的に現在のクォータ情報を返します
+    // Query history records here(if implemented)
+    // Return current quota info temporarily
 
     return {
       totalCost: quota.monthlyCost,
@@ -200,7 +200,7 @@ export class CostControlService {
   }
 
   /**
-   * コスト警告閾値のチェック
+   * Check cost warning threshold
    */
   async checkWarningThreshold(userId: string): Promise<{
     shouldWarn: boolean;
@@ -212,14 +212,14 @@ export class CostControlService {
     if (usagePercent >= 90) {
       return {
         shouldWarn: true,
-        message: `⚠️ クォータ使用率が ${usagePercent.toFixed(1)}% に達しました。残り $${quota.remaining.toFixed(2)}`,
+        message: `⚠️ Quota usage reached ${usagePercent.toFixed(1)}%. Remaining: $${quota.remaining.toFixed(2)}`,
       };
     }
 
     if (usagePercent >= 75) {
       return {
         shouldWarn: true,
-        message: `💡 クォータ使用率 ${usagePercent.toFixed(1)}%。コストの管理に注意してください`,
+        message: `💡 Quota usage at ${usagePercent.toFixed(1)}%. Please monitor your costs carefully`,
       };
     }
 
@@ -230,21 +230,21 @@ export class CostControlService {
   }
 
   /**
-   * コスト表示のフォーマット
+   * Format cost display
    */
   formatCost(cost: number): string {
     return `$${cost.toFixed(2)}`;
   }
 
   /**
-   * 時間表示のフォーマット
+   * Format time display
    */
   formatTime(seconds: number): string {
     if (seconds < 60) {
-      return `${seconds.toFixed(0)}`;
+      return `${seconds.toFixed(0)}s`;
     }
     const minutes = Math.floor(seconds / 60);
     const remainingSeconds = seconds % 60;
-    return `${minutes}分${remainingSeconds.toFixed(0)}秒`;
+    return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
   }
 }

+ 46 - 44
server/src/vision-pipeline/vision-pipeline-cost-aware.service.ts

@@ -1,6 +1,6 @@
 /**
- * Vision Pipeline サービス(コスト制御付き)
- * これは vision-pipeline.service.ts の拡張版であり、コスト制御が統合されています
+ * Vision Pipeline Service (with cost control)
+ * This is an extended version of vision-pipeline.service.ts with integrated cost control
  */
 
 import { Injectable, Logger } from '@nestjs/common';
@@ -15,6 +15,7 @@ import { ModelConfigService } from '../model-config/model-config.service';
 import { PreciseModeOptions, PipelineResult, ProcessingStatus, ModeRecommendation } from './vision-pipeline.interface';
 import { VisionModelConfig, VisionAnalysisResult } from '../vision/vision.interface';
 import { CostControlService } from './cost-control.service';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class VisionPipelineCostAwareService {
@@ -27,11 +28,12 @@ export class VisionPipelineCostAwareService {
     private elasticsearch: ElasticsearchService,
     private modelConfigService: ModelConfigService,
     private configService: ConfigService,
-    private costControl: CostControlService, // 新增成本控制服务
+    private costControl: CostControlService,
+    private i18nService: I18nService,
   ) { }
 
   /**
-   * メイン処理フロー:精密モード(コスト制御付き)
+   * Main processing flow: Precise mode (with cost control)
    */
   async processPreciseMode(
     filePath: string,
@@ -50,12 +52,12 @@ export class VisionPipelineCostAwareService {
     );
 
     try {
-      // ステップ 1: 形式の統一
-      this.updateStatus('converting', 10, 'ドキュメント形式を変換中...');
+      // Step 1: Convert format
+      this.updateStatus('converting', 10, 'Converting document format...');
       pdfPath = await this.convertToPDF(filePath);
 
-      // ステップ 2: PDF から画像への変換
-      this.updateStatus('splitting', 30, 'PDF を画像に変換中...');
+      // Step 2: Convert PDF to images
+      this.updateStatus('splitting', 30, 'Converting PDF to images...');
       const conversionResult = await this.pdf2Image.convertToImages(pdfPath, {
         density: 300,
         quality: 85,
@@ -63,24 +65,24 @@ export class VisionPipelineCostAwareService {
       });
 
       if (conversionResult.images.length === 0) {
-        throw new Error('PDF から画像への変換に失敗しました。画像が生成されませんでした');
+        throw new Error(this.i18nService.getMessage('pdfToImageConversionFailed'));
       }
 
-      // 限制处理页数
+      // Limit processing pages
       imagesToProcess = options.maxPages
         ? conversionResult.images.slice(0, options.maxPages)
         : conversionResult.images;
 
       const pageCount = imagesToProcess.length;
 
-      // ステップ 3: コスト見積もりとクォータチェック
-      this.updateStatus('checking', 40, 'クォータを確認し、コストを見積もり中...');
+      // Step 3: Cost estimation and quota check
+      this.updateStatus('checking', 40, 'Checking quota and estimating cost...');
       const costEstimate = this.costControl.estimateCost(pageCount);
       this.logger.log(
-        `推定コスト: $${costEstimate.estimatedCost.toFixed(2)}, 推定時間: ${this.costControl.formatTime(costEstimate.estimatedTime)}`
+        `Estimated cost: $${costEstimate.estimatedCost.toFixed(2)}, Estimated time: ${this.costControl.formatTime(costEstimate.estimatedTime)}`
       );
 
-      // クォータチェック
+      // Quota check
       const quotaCheck = await this.costControl.checkQuota(
         options.userId,
         costEstimate.estimatedCost,
@@ -90,17 +92,17 @@ export class VisionPipelineCostAwareService {
         throw new Error(quotaCheck.reason);
       }
 
-      // コスト警告チェック
+      // Cost warning check
       const warning = await this.costControl.checkWarningThreshold(options.userId);
       if (warning.shouldWarn) {
         this.logger.warn(warning.message);
       }
 
-      // ステップ 4: Vision モデル設定の取得
+      // Step 4: Get Vision model config
       const modelConfig = await this.getVisionModelConfig(options.userId, options.modelId, options.tenantId);
 
-      // ステップ 5: VL モデル分析
-      this.updateStatus('analyzing', 50, 'ビジョンモデルを使用してページを分析中...');
+      // Step 5: VL model analysis
+      this.updateStatus('analyzing', 50, 'Analyzing pages with Vision model...');
       const batchResult = await this.vision.batchAnalyze(
         imagesToProcess.map(img => img.path),
         modelConfig,
@@ -115,17 +117,17 @@ export class VisionPipelineCostAwareService {
       failedPages = batchResult.failedCount;
       results.push(...batchResult.results);
 
-      // ステップ 6: 実際のコストを差し引く
+      // Step 6: Subtract actual cost
       if (totalCost > 0) {
         await this.costControl.deductQuota(options.userId, totalCost);
-        this.logger.log(`実際に差し引かれたコスト: $${totalCost.toFixed(2)}`);
+        this.logger.log(`Actual cost deducted: $${totalCost.toFixed(2)}`);
       }
 
-      // ステップ 7: 一時ファイルのクリーンアップ
-      this.updateStatus('completed', 100, '処理が完了しました。一時ファイルをクリーンアップ中...');
+      // Step 7: Cleanup temp files
+      this.updateStatus('completed', 100, 'Processing completed. Cleaning up temp files...');
       await this.pdf2Image.cleanupImages(imagesToProcess);
 
-      // PDF に変換した場合、変換後のファイルをクリーンアップ
+      // Cleanup converted PDF file if converted
       if (pdfPath !== filePath) {
         try {
           await fs.unlink(pdfPath);
@@ -156,7 +158,7 @@ export class VisionPipelineCostAwareService {
     } catch (error) {
       this.logger.error(`Precise mode failed: ${error.message}`);
 
-      // 尝试清理临时文件
+      // Try to clean up temp files
       try {
         if (pdfPath !== filePath && pdfPath !== filePath) {
           await fs.unlink(pdfPath);
@@ -182,16 +184,16 @@ export class VisionPipelineCostAwareService {
   }
 
   /**
-   * Vision モデル設定の取得
+   * Get Vision model configuration
    */
   private async getVisionModelConfig(userId: string, modelId: string, tenantId?: string): Promise<VisionModelConfig> {
     const config = await this.modelConfigService.findOne(modelId, userId, tenantId || 'default');
 
     if (!config) {
-      throw new Error(`モデル設定が見つかりません: ${modelId}`);
+      throw new Error(`Model config not found: ${modelId}`);
     }
 
-    // APIキーはオプションです - ローカルモデルを許可します
+    // API key is optional - allows local models
 
     return {
       baseUrl: config.baseUrl || '',
@@ -201,22 +203,22 @@ export class VisionPipelineCostAwareService {
   }
 
   /**
-   * PDF への変換
+   * Convert to PDF
    */
   private async convertToPDF(filePath: string): Promise<string> {
     const ext = path.extname(filePath).toLowerCase();
 
-    // 既に PDF の場合はそのまま返す
+    // Return as-is if already PDF
     if (ext === '.pdf') {
       return filePath;
     }
 
-    // LibreOffice を呼び出して変換
+    // Call LibreOffice to convert
     return await this.libreOffice.convertToPDF(filePath);
   }
 
   /**
-   * 形式検出とモードの推奨(コスト見積もり付き)
+   * Format detection and mode recommendation (with cost estimation)
    */
   async recommendMode(filePath: string): Promise<ModeRecommendation> {
     const ext = path.extname(filePath).toLowerCase();
@@ -229,46 +231,46 @@ export class VisionPipelineCostAwareService {
     if (!supportedFormats.includes(ext)) {
       return {
         recommendedMode: 'fast',
-        reason: `サポートされていないファイル形式です: ${ext}`,
-        warnings: ['高速モード(テキスト抽出のみ)を使用します'],
+        reason: `Unsupported file format: ${ext}`,
+        warnings: ['Using fast mode (text extraction only)'],
       };
     }
 
     if (!preciseFormats.includes(ext)) {
       return {
         recommendedMode: 'fast',
-        reason: `形式 ${ext} は精密モードをサポートしていません`,
-        warnings: ['高速モード(テキスト抽出のみ)を使用します'],
+        reason: `Format ${ext} does not support precise mode`,
+        warnings: ['Using fast mode (text extraction only)'],
       };
     }
 
-    // ページ数の見積もり(ファイルサイズに基づく
+    // Estimate page count(based on file size
     const estimatedPages = Math.max(1, Math.ceil(sizeMB * 2));
     const costEstimate = this.costControl.estimateCost(estimatedPages);
 
-    // ファイルサイズが大きい場合は精密モードを推奨
+    // Recommend precise mode for large files
     if (sizeMB > 50) {
       return {
         recommendedMode: 'precise',
-        reason: 'ファイルが大きいため、完全な情報を保持するために精密モードを推奨します',
+        reason: 'File is large, recommend precise mode to preserve full content',
         estimatedCost: costEstimate.estimatedCost,
         estimatedTime: costEstimate.estimatedTime,
-        warnings: ['処理時間が長くなる可能性があります', 'API 費用が発生します'],
+        warnings: ['Processing time may be longer', 'API costs will be incurred'],
       };
     }
 
-    // 精密モードを推奨
+    // Recommend precise mode
     return {
       recommendedMode: 'precise',
-      reason: '精密モードが利用可能です。テキストと画像の混合コンテンツを保持できます',
+      reason: 'Precise mode available. Can preserve mixed text and image content',
       estimatedCost: costEstimate.estimatedCost,
       estimatedTime: costEstimate.estimatedTime,
-      warnings: ['API 費用が発生します'],
+      warnings: ['API costs will be incurred'],
     };
   }
 
   /**
-   * ユーザーのクォータ情報を取得
+   * Get user quota information
    */
   async getUserQuotaInfo(userId: string) {
     const quota = await this.costControl.getUserQuota(userId);
@@ -282,7 +284,7 @@ export class VisionPipelineCostAwareService {
   }
 
   /**
-   * 処理状態の更新(リアルタイムフィードバック用)
+   * Update processing status (for real-time feedback)
    */
   private updateStatus(status: ProcessingStatus['status'], progress: number, message: string): void {
     this.logger.log(`[${status}] ${progress}% - ${message}`);

+ 3 - 3
server/src/vision-pipeline/vision-pipeline.interface.ts

@@ -1,5 +1,5 @@
 /**
- * Vision Pipeline 接口定义
+ * Vision Pipeline Interface Definitions
  */
 
 import { VisionAnalysisResult } from '../vision/vision.interface';
@@ -23,7 +23,7 @@ export interface PipelineResult {
   failedPages: number;
   results: VisionAnalysisResult[];
   cost: number;
-  duration: number; // 
+  duration: number; // seconds
   mode: 'precise';
 }
 
@@ -44,6 +44,6 @@ export interface ModeRecommendation {
   recommendedMode: 'precise' | 'fast';
   reason: string;
   estimatedCost?: number;
-  estimatedTime?: number; // 
+  estimatedTime?: number; // seconds
   warnings?: string[];
 }

+ 3 - 1
server/src/vision-pipeline/vision-pipeline.service.ts

@@ -16,6 +16,7 @@ import {
   VisionAnalysisResult,
   VisionModelConfig,
 } from '../vision/vision.interface';
+import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
 export class VisionPipelineService {
@@ -27,6 +28,7 @@ export class VisionPipelineService {
     private vision: VisionService,
     private modelConfigService: ModelConfigService,
     private configService: ConfigService,
+    private i18nService: I18nService,
   ) { }
 
   /**
@@ -66,7 +68,7 @@ export class VisionPipelineService {
       });
 
       if (conversionResult.images.length === 0) {
-        throw new Error('Failed to convert PDF to images. No images were generated.');
+        throw new Error(this.i18nService.getMessage('pdfToImageConversionFailed'));
       }
 
       this.logger.log(

+ 10 - 10
server/src/vision/vision.interface.ts

@@ -1,19 +1,19 @@
 /**
- * Vision 服务接口定义
+ * Vision Service Interface Definitions
  */
 
 export interface VisionAnalysisResult {
-  text: string;              // 抽出されたテキスト内容
-  images: ImageDescription[]; // 画像の説明
-  layout: string;            // レイアウトの種類
-  confidence: number;        // 信頼度 (0-1)
-  pageIndex?: number;        // 页码
+  text: string;              // Extracted text content
+  images: ImageDescription[]; // Image description
+  layout: string;            // Layout type
+  confidence: number;        // Confidence (0-1)
+  pageIndex?: number;        // Page number
 }
 
 export interface ImageDescription {
-  type: string;              // 图片类型 (图表/架构图/流程图等)
-  description: string;       // 详细描述
-  position?: number;         // ページ内での位置
+  type: string;              // Image type (chart/diagram/flowchart etc.)
+  description: string;       // Detailed description
+  position?: number;         // Position in page
 }
 
 export interface VisionModelConfig {
@@ -27,7 +27,7 @@ export interface BatchAnalysisResult {
   totalPages: number;
   successCount: number;
   failedCount: number;
-  estimatedCost: number;     // 预估成本(美元)
+  estimatedCost: number;     // Estimated cost(USD)
 }
 
 export interface PageQuality {

+ 48 - 48
server/src/vision/vision.service.ts

@@ -16,7 +16,7 @@ export class VisionService {
   ) { }
 
   /**
-   * 単一画像の分析(ドキュメントページ)
+   * Analyze single image (document page)
    */
   async analyzeImage(
     imagePath: string,
@@ -24,7 +24,7 @@ export class VisionService {
     pageIndex?: number,
   ): Promise<VisionAnalysisResult> {
     const maxRetries = 3;
-    const baseDelay = 3000; // 3秒の基礎遅延
+    const baseDelay = 3000; // 3 second base delay
 
     for (let attempt = 1; attempt <= maxRetries; attempt++) {
       try {
@@ -36,21 +36,21 @@ export class VisionService {
           throw new Error(this.i18nService.formatMessage('visionAnalysisFailed', { message: error.message }));
         }
 
-        const delay = baseDelay + Math.random() * 2000; // 3-5秒のランダムな遅延
+        const delay = baseDelay + Math.random() * 2000; // 3-5 second random delay
         this.logger.warn(
-          `⚠️ 第 ${pageIndex || '?'} ページの分析に失敗しました (${attempt}/${maxRetries}), ${delay.toFixed(0)}ms後に再試行します: ${error.message}`
+          `⚠️ Failed to analyze page ${pageIndex || '?'} (${attempt}/${maxRetries}), retrying in ${delay.toFixed(0)}ms: ${error.message}`
         );
 
         await this.sleep(delay);
       }
     }
 
-    // この行は理論的には実行されませんが、TypeScript の要求を満たすために記述しています
+    // This line theoretically should not execute, but included to satisfy TypeScript
     throw new Error(this.i18nService.getMessage('retryMechanismError'));
   }
 
   /**
-   * 実際の画像分析を実行
+   * Perform actual image analysis
    */
   private async performAnalysis(
     imagePath: string,
@@ -58,22 +58,22 @@ export class VisionService {
     pageIndex?: number,
   ): Promise<VisionAnalysisResult> {
     try {
-      // 画像を読み込み、base64 に変換
+      // Load image and convert to base64
       const imageBuffer = await fs.readFile(imagePath);
       const base64Image = imageBuffer.toString('base64');
       const mimeType = this.getMimeType(imagePath);
 
-      // ビジョンモデルのインスタンスを作成
+      // Create vision model instance
       const model = new ChatOpenAI({
         apiKey: modelConfig.apiKey,
         model: modelConfig.modelId,
         configuration: {
           baseURL: modelConfig.baseUrl,
         },
-        temperature: 0.1, // ランダム性を抑え、一貫性を高める
+        temperature: 0.1, // Reduce randomness, increase consistency
       });
 
-      // 専門的なドキュメント分析プロンプトを構築
+      // Build professional document analysis prompt
       const systemPrompt = this.i18nService.getMessage('visionSystemPrompt');
 
       const message = new HumanMessage({
@@ -91,15 +91,15 @@ export class VisionService {
         ],
       });
 
-      // モデルの呼び出し
+      // Call model
       this.logger.log(this.i18nService.formatMessage('visionModelCall', { model: modelConfig.modelId, page: pageIndex || 'single' }));
       const response = await model.invoke([message]);
       let content = response.content as string;
 
-      // JSON の解析を試行
+      // Try to parse JSON
       let result: VisionAnalysisResult;
       try {
-        // Markdown のコードブロックタグをクリーンアップ
+        // Clean up markdown code block tags
         content = content.replace(/```json/g, '').replace(/```/g, '').trim();
         const parsed = JSON.parse(content);
 
@@ -111,7 +111,7 @@ export class VisionService {
           pageIndex,
         };
       } catch (parseError) {
-        // 解析に失敗した場合は、内容全体をテキストとして扱う
+        // If parsing fails, treat entire content as text
         this.logger.warn(`Failed to parse JSON response for ${imagePath}, using raw text`);
         result = {
           text: content,
@@ -125,7 +125,7 @@ export class VisionService {
       this.logger.log(
         this.i18nService.formatMessage('visionAnalysisSuccess', {
           path: imagePath,
-          page: pageIndex ? ` (第 ${pageIndex} ページ)` : '',
+          page: pageIndex ? ` (page ${pageIndex})` : '',
           textLen: result.text.length,
           imgCount: result.images.length,
           layout: result.layout,
@@ -135,28 +135,28 @@ export class VisionService {
 
       return result;
     } catch (error) {
-      throw error; // 重新抛出错误供重试机制处理
+      throw error; // Re-throw error for retry mechanism
     }
   }
 
   /**
-   * 再試行可能なエラーかどうかを判断
+   * Determine if error is retryable
    */
   private isRetryableError(error: any): boolean {
     const errorMessage = error.message?.toLowerCase() || '';
     const errorCode = error.status || error.code;
 
-    // 429 レート制限エラー
-    if (errorCode === 429 || errorMessage.includes('rate limit') || errorMessage.includes('リクエストが多すぎます')) {
+    // 429 rate limit error
+    if (errorCode === 429 || errorMessage.includes('rate limit') || errorMessage.includes('too many requests')) {
       return true;
     }
 
-    // 5xx サーバーエラー
+    // 5xx server error
     if (errorCode >= 500 && errorCode < 600) {
       return true;
     }
 
-    // ネットワーク関連エラー
+    // Network related error
     if (errorMessage.includes('timeout') || errorMessage.includes('network') || errorMessage.includes('connection')) {
       return true;
     }
@@ -165,14 +165,14 @@ export class VisionService {
   }
 
   /**
-   * 遅延関数
+   * Sleep function
    */
   private sleep(ms: number): Promise<void> {
     return new Promise(resolve => setTimeout(resolve, ms));
   }
 
   /**
-   * 複数画像の一括分析
+   * Batch analyze multiple images
    */
   async batchAnalyze(
     imagePaths: string[],
@@ -189,34 +189,34 @@ export class VisionService {
     let failedCount = 0;
 
     this.logger.log(this.i18nService.formatMessage('batchAnalysisStarted', { count: imagePaths.length }));
-    this.logger.log(`🔧 モデル設定: ${modelConfig.modelId} (${modelConfig.baseUrl || 'OpenAI'})`);
+    this.logger.log(`🔧 Model config: ${modelConfig.modelId} (${modelConfig.baseUrl || 'OpenAI'})`);
 
     for (let i = 0; i < imagePaths.length; i++) {
       const imagePath = imagePaths[i];
       const pageIndex = startIndex + i;
       const progress = Math.round(((i + 1) / imagePaths.length) * 100);
 
-      this.logger.log(`🖼️  第 ${pageIndex} ページを分析中 (${i + 1}/${imagePaths.length}, ${progress}%)`);
+      this.logger.log(`🖼️  Analyzing page ${pageIndex} (${i + 1}/${imagePaths.length}, ${progress}%)`);
 
-      // 進捗コールバックを呼び出し
+      // Call progress callback
       if (onProgress) {
         onProgress(i + 1, imagePaths.length);
       }
 
-      // 品質チェック(スキップ時は直接分析)
+      // Quality check(skip analysis if skipped)
       if (!skipQualityCheck) {
         const quality = await this.checkImageQuality(imagePath);
         if (!quality.isGood) {
-          this.logger.warn(`⚠️  第 ${pageIndex} ページをスキップ(品質不良): ${quality.reason}`);
+          this.logger.warn(`⚠️  Skipped page ${pageIndex} (poor quality): ${quality.reason}`);
           failedCount++;
           continue;
         } else {
-          this.logger.log(`✅ 第 ${pageIndex} ページの品質チェック合格 (スコア: ${(quality.score || 0).toFixed(2)})`);
+          this.logger.log(`✅ Page ${pageIndex} quality check passed (score: ${(quality.score || 0).toFixed(2)})`);
         }
       }
 
       try {
-        this.logger.log(`🔍 Vision モデルで第 ${pageIndex} ページを分析中...`);
+        this.logger.log(`🔍 Analyzing page ${pageIndex} with Vision model...`);
         const startTime = Date.now();
         const result = await this.analyzeImage(imagePath, modelConfig, pageIndex);
         const duration = ((Date.now() - startTime) / 1000).toFixed(1);
@@ -225,13 +225,13 @@ export class VisionService {
         successCount++;
 
         this.logger.log(
-          `✅ 第 ${pageIndex} ページ分析完了 (所要時間: ${duration}s, ` +
-          `テキスト: ${result.text.length}文字, ` +
-          `画像数: ${result.images.length}個, ` +
-          `信頼度: ${(result.confidence * 100).toFixed(1)}%)`
+          `✅ Page ${pageIndex} analysis completed (time: ${duration}s, ` +
+          `text: ${result.text.length} chars, ` +
+          `images: ${result.images.length}, ` +
+          `confidence: ${(result.confidence * 100).toFixed(1)}%)`
         );
 
-        // 結果付きで進捗コールバックを呼び出し
+        // Call progress callback with result
         if (onProgress) {
           onProgress(i + 1, imagePaths.length, result);
         }
@@ -241,13 +241,13 @@ export class VisionService {
       }
     }
 
-    // 推定コストの計算(1枚あたり $0.01 と仮定)
+    // Calculate estimated cost (assuming $0.01 per image)
     const estimatedCost = successCount * 0.01;
 
     this.logger.log(
-      `🎉 Vision 一括分析完了! ` +
-      `✅ 成功: ${successCount} ページ, ❌ 失敗: ${failedCount} ページ, ` +
-      `💰 推定コスト: $${estimatedCost.toFixed(2)}`
+      `🎉 Vision batch analysis completed! ` +
+      `✅ Success: ${successCount} pages, ❌ Failed: ${failedCount} pages, ` +
+      `💰 Estimated cost: $${estimatedCost.toFixed(2)}`
     );
 
     return {
@@ -260,24 +260,24 @@ export class VisionService {
   }
 
   /**
-   * 画像品質のチェック
+   * Check image quality
    */
   async checkImageQuality(imagePath: string): Promise<{ isGood: boolean; reason?: string; score?: number }> {
     try {
       const stats = await fs.stat(imagePath);
       const sizeKB = stats.size / 1024;
 
-      // ファイルサイズのチェック(5KB以上)
+      // Check file size(5KB+)
       if (sizeKB < 5) {
-        return { isGood: false, reason: `ファイルが小さすぎます (${sizeKB.toFixed(2)}KB)`, score: 0 };
+        return { isGood: false, reason: `File too small (${sizeKB.toFixed(2)}KB)`, score: 0 };
       }
 
-      // ファイルサイズ上限のチェック(10MB)
+      // Check file size limit(10MB)
       if (sizeKB > 10240) {
-        return { isGood: false, reason: `ファイルが大きすぎます (${sizeKB.toFixed(2)}KB)`, score: 0 };
+        return { isGood: false, reason: `File too large (${sizeKB.toFixed(2)}KB)`, score: 0 };
       }
 
-      // 簡易的な品質スコアリング
+      // Simple quality scoring
       let score = 0.5;
       if (sizeKB > 50) score += 0.2;
       if (sizeKB > 100) score += 0.2;
@@ -292,7 +292,7 @@ export class VisionService {
   }
 
   /**
-   * サポートされている画像ファイルかどうかを確認
+   * Check if file is a supported image format
    */
   isImageFile(mimetype: string): boolean {
     const imageMimeTypes = [
@@ -307,7 +307,7 @@ export class VisionService {
   }
 
   /**
-   * MIME タイプを取得
+   * Get MIME type
    */
   private getMimeType(filePath: string): string {
     const ext = filePath.toLowerCase().split('.').pop();
@@ -325,7 +325,7 @@ export class VisionService {
   }
 
   /**
-   * 旧インターフェース互換:単一画像の内容を抽出
+   * Legacy interface compatibility: extract content from single image
    */
   async extractImageContent(
     imagePath: string,