This commit is contained in:
syui 2025-05-26 14:43:11 +09:00
parent dcec8db031
commit d6b9889135
Signed by: syui
GPG Key ID: 5417CFEBAD92DF56
3 changed files with 3 additions and 552 deletions

View File

@ -227,9 +227,6 @@ Example memory:
• 中期記憶MTM繰り返し登場する話題、圧縮された文脈保持
• 長期記憶LTM信頼・関係・背景知識、恒久的な人格情報
### 2.3 選択的記憶保持戦略Selective Retention Strategy
• 重要度評価Importance Score
• 希少性・再利用頻度による重み付け

View File

@ -1,549 +0,0 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>改良版 ChatGPT会話コンバーター</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.header h1 {
color: #2c3e50;
margin: 0;
font-size: 2.5em;
font-weight: 300;
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
}
.upload-area {
border: 3px dashed #3498db;
border-radius: 15px;
padding: 40px 20px;
text-align: center;
background: linear-gradient(45deg, #f8f9ff, #e8f4f8);
margin-bottom: 30px;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #2980b9;
background: linear-gradient(45deg, #f0f8ff, #e0f0f8);
transform: translateY(-2px);
}
.upload-area.dragover {
border-color: #27ae60;
background: linear-gradient(45deg, #f0fff0, #e0f8e0);
}
.upload-icon {
font-size: 4em;
color: #3498db;
margin-bottom: 20px;
display: block;
}
input[type="file"] {
display: none;
}
.btn {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
margin: 10px;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4);
}
.btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 30px 0;
}
.stat-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
.stat-value {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9em;
opacity: 0.9;
}
.progress-bar {
width: 100%;
height: 8px;
background: #ecf0f1;
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3498db, #27ae60);
transition: width 0.3s ease;
width: 0%;
}
.log {
background: #2c3e50;
color: #ecf0f1;
padding: 20px;
border-radius: 10px;
height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
margin-top: 20px;
white-space: pre-wrap;
}
.error {
color: #e74c3c;
}
.success {
color: #27ae60;
}
.warning {
color: #f39c12;
}
.info {
color: #3498db;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.processing {
animation: pulse 2s infinite;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔧 改良版 ChatGPT会話コンバーター</h1>
<p>画像・検索・特殊メッセージに対応した堅牢な変換ツール</p>
</div>
<div class="upload-area" onclick="document.getElementById('file-input').click()">
<span class="upload-icon">📁</span>
<h3>ChatGPT会話ファイルをドロップまたはクリックして選択</h3>
<p>conversations.json ファイルをアップロード</p>
<input type="file" id="file-input" accept=".json" />
</div>
<div class="stats" id="stats" style="display: none;">
<div class="stat-card">
<div class="stat-value" id="total-conversations">0</div>
<div class="stat-label">総会話数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="processed-conversations">0</div>
<div class="stat-label">処理済み会話</div>
</div>
<div class="stat-card">
<div class="stat-value" id="success-conversations">0</div>
<div class="stat-label">変換成功</div>
</div>
<div class="stat-card">
<div class="stat-value" id="failed-conversations">0</div>
<div class="stat-label">変換失敗</div>
</div>
</div>
<div class="progress-bar" id="progress-container" style="display: none;">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div style="text-align: center;">
<button class="btn" id="convert-btn" disabled>🔄 変換開始</button>
<button class="btn" id="download-btn" disabled style="background: linear-gradient(135deg, #27ae60, #2ecc71);">📥 結果をダウンロード</button>
<button class="btn" id="clear-btn" style="background: linear-gradient(135deg, #e74c3c, #c0392b);">🗑️ クリア</button>
</div>
<div class="log" id="log"></div>
</div>
<script>
let originalData = null;
let convertedResults = [];
// DOM要素
const fileInput = document.getElementById('file-input');
const uploadArea = document.querySelector('.upload-area');
const convertBtn = document.getElementById('convert-btn');
const downloadBtn = document.getElementById('download-btn');
const clearBtn = document.getElementById('clear-btn');
const logElement = document.getElementById('log');
const statsElement = document.getElementById('stats');
const progressContainer = document.getElementById('progress-container');
const progressFill = document.getElementById('progress-fill');
// 統計要素
const totalConversationsEl = document.getElementById('total-conversations');
const processedConversationsEl = document.getElementById('processed-conversations');
const successConversationsEl = document.getElementById('success-conversations');
const failedConversationsEl = document.getElementById('failed-conversations');
// ドラッグ&ドロップ
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
});
// ファイル選択
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
// ファイル処理
function handleFile(file) {
if (!file.name.endsWith('.json')) {
log('❌ JSONファイルを選択してください', 'error');
return;
}
log(`📁 ファイルを読み込み中: ${file.name}`, 'info');
const reader = new FileReader();
reader.onload = (e) => {
try {
originalData = JSON.parse(e.target.result);
log(`✅ ファイル読み込み完了 (${(file.size / 1024 / 1024).toFixed(2)}MB)`, 'success');
// 統計表示
const totalCount = Array.isArray(originalData) ? originalData.length : 1;
totalConversationsEl.textContent = totalCount;
statsElement.style.display = 'grid';
convertBtn.disabled = false;
log('🔄 変換準備完了。「変換開始」ボタンをクリックしてください', 'info');
} catch (error) {
log(`❌ JSONファイルの解析に失敗: ${error.message}`, 'error');
}
};
reader.readAsText(file);
}
// ログ出力
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const className = type;
logElement.innerHTML += `<span class="${className}">[${timestamp}] ${message}</span>\n`;
logElement.scrollTop = logElement.scrollHeight;
}
// メッセージの内容を安全に取得
function extractMessageContent(message) {
if (!message || !message.content) return '';
const content = message.content;
// テキストコンテンツの場合
if (content.content_type === 'text' && content.parts) {
return content.parts
.filter(part => part && typeof part === 'string' && part.trim())
.join('\n')
.trim();
}
// マルチモーダル(画像付き)コンテンツの場合
if (content.content_type === 'multimodal_text' && content.parts) {
const textParts = [];
for (const part of content.parts) {
if (typeof part === 'string' && part.trim()) {
textParts.push(part);
} else if (part && typeof part === 'object') {
// 画像や他のメディアの場合
if (part.image_url) {
textParts.push('[画像が添付されています]');
} else if (part.type === 'text' && part.text) {
textParts.push(part.text);
}
}
}
return textParts.join('\n').trim();
}
// ユーザープロファイル情報の場合
if (content.content_type === 'user_editable_context') {
return '[システム設定情報]';
}
// その他の特殊コンテンツ
if (content.content_type && content.content_type !== 'text') {
return `[${content.content_type}]`;
}
return '';
}
// 会話の線形化(親子関係を辿って順序付け)
function linearizeConversation(mapping) {
const messages = [];
const visited = new Set();
// ルートノードを見つける
const rootNode = Object.values(mapping).find(node => node.parent === null);
if (!rootNode) {
return messages;
}
// 深度優先探索で会話を辿る
function traverse(nodeId) {
if (visited.has(nodeId) || !mapping[nodeId]) {
return;
}
visited.add(nodeId);
const node = mapping[nodeId];
// メッセージが存在し、有効なコンテンツがある場合のみ追加
if (node.message) {
const message = node.message;
const content = extractMessageContent(message);
// 以下の条件で有効なメッセージとして扱う
const isValid = content &&
content.length > 0 &&
content !== '[システム設定情報]' &&
(!message.metadata?.is_visually_hidden_from_conversation ||
(message.author?.role === 'user' || message.author?.role === 'assistant'));
if (isValid) {
messages.push({
role: message.author?.role || 'unknown',
content: content,
timestamp: message.create_time || message.update_time || Date.now() / 1000
});
}
}
// 子ードを処理通常は1つだが、分岐がある場合もある
if (node.children && node.children.length > 0) {
// 最初の子ノードのみを辿る(最も新しい応答を優先)
traverse(node.children[0]);
}
}
traverse(rootNode.id);
return messages;
}
// 単一会話の変換
function convertSingleConversation(conversation, index) {
try {
if (!conversation.mapping) {
throw new Error('mapping が見つかりません');
}
// 会話を線形化
const messages = linearizeConversation(conversation.mapping);
if (messages.length === 0) {
throw new Error('有効なメッセージが見つかりません');
}
// 結果の構築
const result = {
title: conversation.title || `会話 ${index + 1}`,
create_time: conversation.create_time || Date.now() / 1000,
update_time: conversation.update_time || Date.now() / 1000,
conversation_id: conversation.conversation_id || conversation.id || `conv_${index}`,
messages: messages,
metadata: {
original_message_count: Object.keys(conversation.mapping).length,
processed_message_count: messages.length,
is_archived: conversation.is_archived || false,
model_slug: conversation.default_model_slug || 'unknown'
}
};
return { success: true, result, error: null };
} catch (error) {
return {
success: false,
result: null,
error: error.message,
conversation_title: conversation.title || `会話 ${index + 1}`
};
}
}
// 変換処理
convertBtn.addEventListener('click', async () => {
if (!originalData) return;
convertBtn.disabled = true;
downloadBtn.disabled = true;
convertedResults = [];
const conversations = Array.isArray(originalData) ? originalData : [originalData];
const total = conversations.length;
let processed = 0;
let success = 0;
let failed = 0;
log(`🔄 ${total}個の会話の変換を開始します...`, 'info');
progressContainer.style.display = 'block';
for (let i = 0; i < conversations.length; i++) {
const conversation = conversations[i];
log(`[${i + 1}/${total}] "${conversation.title || `会話${i + 1}`}" を処理中...`, 'info');
const result = convertSingleConversation(conversation, i);
if (result.success) {
convertedResults.push(result.result);
success++;
log(`✅ [${i + 1}/${total}] 変換成功: ${result.result.messages.length}メッセージ`, 'success');
} else {
failed++;
log(`❌ [${i + 1}/${total}] 変換失敗: ${result.error}`, 'error');
}
processed++;
// 統計更新
processedConversationsEl.textContent = processed;
successConversationsEl.textContent = success;
failedConversationsEl.textContent = failed;
// プログレスバー更新
const progress = (processed / total) * 100;
progressFill.style.width = `${progress}%`;
// UIを更新するため少し待機
await new Promise(resolve => setTimeout(resolve, 1));
}
log(`🎉 変換完了! 成功: ${success}個, 失敗: ${failed}個`, success > 0 ? 'success' : 'warning');
if (success > 0) {
downloadBtn.disabled = false;
}
convertBtn.disabled = false;
});
// ダウンロード
downloadBtn.addEventListener('click', () => {
if (convertedResults.length === 0) return;
const blob = new Blob([JSON.stringify(convertedResults, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chatgpt_conversations_converted_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
log('📥 変換結果をダウンロードしました', 'success');
});
// クリア
clearBtn.addEventListener('click', () => {
originalData = null;
convertedResults = [];
logElement.innerHTML = '';
statsElement.style.display = 'none';
progressContainer.style.display = 'none';
progressFill.style.width = '0%';
// ボタン状態リセット
convertBtn.disabled = true;
downloadBtn.disabled = true;
// ファイル入力リセット
fileInput.value = '';
log('🗑️ すべてクリアしました', 'info');
});
// 初期メッセージ
log('👋 ChatGPT会話コンバーターへようこそ', 'info');
log('📁 conversations.json ファイルをアップロードしてください', 'info');
</script>
</body>
</html>

View File

@ -1,5 +1,8 @@
# rerequirements.txt
fastapi>=0.104.0
uvicorn[standard]>=0.24.0
pydantic>=2.5.0
requests>=2.31.0
python-multipart>=0.0.6
aiohttp
asyncio