キャラセリフ稼働可能
This commit is contained in:
@@ -1,24 +1,52 @@
|
||||
package com.example.curation_train_app
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.json.JSONArray
|
||||
|
||||
object AiReplyParser {
|
||||
|
||||
fun parse(json: String): AiReply {
|
||||
return try {
|
||||
fun extractTextFromResponse(json: String): String {
|
||||
|
||||
val obj = JSONObject(json)
|
||||
return try {
|
||||
// 配列としてパース
|
||||
val arr = JSONObject("{\"a\":$json}").getJSONArray("a")
|
||||
val obj = arr.getJSONObject(0)
|
||||
|
||||
// GPT から返ってくるのは「text」だけ
|
||||
val text = obj.optString("text", "(解析エラー)")
|
||||
// textだけ取得
|
||||
var text = obj.getString("text")
|
||||
|
||||
// emotion は入っていないため、常に NORMAL を返す
|
||||
val emotion = EmotionType.NORMAL
|
||||
// ---- 余計なものを完全除去 ----
|
||||
text = text
|
||||
.replace("text\":", "") // "text":
|
||||
.replace("\"text\":\"", "") // "text":"
|
||||
.replace("{\"text\":\"", "") // {"text":"
|
||||
.replace("\"}", "") // "}
|
||||
.replace("}", "") // }
|
||||
.replace("{", "") // {
|
||||
.replace("\\n", "") // 改行タグ
|
||||
.replace("\n", "")
|
||||
.replace("emotion", "") // emotion行
|
||||
.replace("normal", "")
|
||||
.replace("happy", "")
|
||||
.replace("angry", "")
|
||||
.replace("surprised", "")
|
||||
.replace("confused", "")
|
||||
.replace("sleepy", "")
|
||||
.replace("worry", "")
|
||||
.replace("think", "")
|
||||
.replace("“", "")
|
||||
.replace("”", "")
|
||||
.replace("\"", "")
|
||||
.replace(",", "")
|
||||
.replace(":", "")
|
||||
|
||||
AiReply(text, emotion)
|
||||
.trim()
|
||||
|
||||
} catch (e: Exception) {
|
||||
AiReply("(JSON解析エラー)", EmotionType.NORMAL)
|
||||
text
|
||||
|
||||
} catch (e: Exception) {
|
||||
json
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,4 +20,7 @@ class AkaneResponder : CharacterResponder {
|
||||
"なんか気になる情報があるよねっ!気をつけていこーっ!"
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(text: String): String = respond(text, InfoType.NORMAL)
|
||||
|
||||
}
|
||||
|
||||
@@ -25,19 +25,15 @@ class CharacterReplyManager {
|
||||
?: return "キャラが見つかりません。"
|
||||
return responder.respond(info, type)
|
||||
}
|
||||
|
||||
fun replyWithAI(characterId: String, info: String, type: InfoType): AiReply {
|
||||
fun replyWithAI(characterId: String, info: String, type: InfoType): String {
|
||||
val character = CharacterProfiles.getProfile(characterId)
|
||||
?: return AiReply("(エラー:キャラが不明です)", EmotionType.NORMAL)
|
||||
?: return "(エラー:キャラが不明です)"
|
||||
|
||||
|
||||
val prompt = PromptBuilder.buildPrompt(character, info, type)
|
||||
|
||||
val ai = AiClient(apiKey = API_KEY)
|
||||
val raw = ai.requestCharacterReply(prompt)
|
||||
|
||||
// JSON パース
|
||||
return AiReplyParser.parse(raw)
|
||||
val ai = AiClient("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")
|
||||
return ai.requestCharacterReply(prompt)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,15 +3,172 @@ package com.example.curation_train_app
|
||||
object DummyLines {
|
||||
|
||||
fun linesOf(region: String): List<String> {
|
||||
return when (region) {
|
||||
"北海道" -> listOf("JR北海道全線", "札幌市営地下鉄", "道南いさりび鉄道")
|
||||
"東北" -> listOf("JR東日本(東北)", "仙台市地下鉄", "青い森鉄道")
|
||||
"関東" -> listOf("JR東日本(関東)", "東京メトロ", "京急", "東急", "小田急", "京王")
|
||||
"中部" -> listOf("JR東海", "名鉄", "近鉄(名古屋)")
|
||||
"関西" -> listOf("JR西日本(関西)", "大阪メトロ", "阪急", "阪神", "京阪", "南海", "近鉄(関西)")
|
||||
"中国" -> listOf("JR西日本(中国)", "広島電鉄", "一畑電車")
|
||||
"四国" -> listOf("JR四国")
|
||||
"九州" -> listOf("JR九州", "西鉄")
|
||||
return when(region) {
|
||||
|
||||
// -------------------------
|
||||
// 北海道
|
||||
// -------------------------
|
||||
"hokkaido" -> listOf(
|
||||
"JR北海道(函館本線)",
|
||||
"JR北海道(千歳線)",
|
||||
"JR北海道(札沼線・学園都市線)",
|
||||
"JR北海道(根室本線)",
|
||||
"JR北海道(宗谷本線)",
|
||||
"札幌市営地下鉄 南北線",
|
||||
"札幌市営地下鉄 東西線",
|
||||
"札幌市営地下鉄 東豊線"
|
||||
)
|
||||
|
||||
// -------------------------
|
||||
// 東北
|
||||
// -------------------------
|
||||
"tohoku" -> listOf(
|
||||
"JR東日本(東北本線)",
|
||||
"JR東日本(常磐線)",
|
||||
"JR東日本(奥羽本線)",
|
||||
"JR東日本(仙山線)",
|
||||
"JR東日本(磐越西線)",
|
||||
"仙台市地下鉄 南北線",
|
||||
"仙台市地下鉄 東西線"
|
||||
)
|
||||
|
||||
// -------------------------
|
||||
// 関東
|
||||
// -------------------------
|
||||
"kanto" -> listOf(
|
||||
// JR
|
||||
"JR東日本(山手線)",
|
||||
"JR東日本(中央線快速)",
|
||||
"JR東日本(総武線各停)",
|
||||
"JR東日本(京浜東北線)",
|
||||
"JR東日本(常磐線)",
|
||||
"JR東日本(埼京線)",
|
||||
"JR東日本(横須賀線)",
|
||||
"JR東日本(湘南新宿ライン)",
|
||||
"JR東日本(京葉線)",
|
||||
"JR東日本(武蔵野線)",
|
||||
|
||||
// 私鉄
|
||||
"東京メトロ(丸ノ内線)",
|
||||
"東京メトロ(東西線)",
|
||||
"東京メトロ(日比谷線)",
|
||||
"東京メトロ(半蔵門線)",
|
||||
"東京メトロ(南北線)",
|
||||
"都営地下鉄(浅草線)",
|
||||
"都営地下鉄(三田線)",
|
||||
"東武東上線",
|
||||
"東武伊勢崎線(スカイツリーライン)",
|
||||
"西武池袋線",
|
||||
"西武新宿線",
|
||||
"小田急小田原線",
|
||||
"京王京王線",
|
||||
"東急東横線",
|
||||
"東急田園都市線",
|
||||
"京急本線",
|
||||
"相鉄本線",
|
||||
"つくばエクスプレス"
|
||||
)
|
||||
|
||||
// -------------------------
|
||||
// 中部(東海)
|
||||
// -------------------------
|
||||
"chubu" -> listOf(
|
||||
// JR
|
||||
"JR東海(東海道本線 静岡)",
|
||||
"JR東海(東海道本線 名古屋)",
|
||||
"JR東海(中央本線)",
|
||||
"JR東海(関西本線)",
|
||||
"JR東海(高山本線)",
|
||||
"JR東海(御殿場線)",
|
||||
|
||||
// 名古屋の私鉄
|
||||
"名古屋市営地下鉄 東山線",
|
||||
"名古屋市営地下鉄 名城線",
|
||||
"名古屋市営地下鉄 桜通線",
|
||||
"名鉄名古屋本線",
|
||||
"名鉄犬山線",
|
||||
"名鉄常滑線",
|
||||
"近鉄名古屋線"
|
||||
)
|
||||
|
||||
|
||||
// -------------------------
|
||||
// 関西(あなたの本命)
|
||||
// -------------------------
|
||||
"kansai" -> listOf(
|
||||
// JR西日本
|
||||
"JR西日本(阪和線)",
|
||||
"JR西日本(大阪環状線)",
|
||||
"JR西日本(大和路線)",
|
||||
"JR西日本(関空・紀州路快速)",
|
||||
"JR西日本(京都線)",
|
||||
"JR西日本(神戸線)",
|
||||
"JR西日本(宝塚線)",
|
||||
"JR西日本(琵琶湖線)",
|
||||
|
||||
// 南海
|
||||
"南海本線",
|
||||
"南海空港線",
|
||||
"南海高野線",
|
||||
"南海加太線",
|
||||
|
||||
// 泉北高速
|
||||
"泉北高速鉄道線",
|
||||
|
||||
// Osaka Metro
|
||||
"Osaka Metro 御堂筋線",
|
||||
"Osaka Metro 四つ橋線",
|
||||
"Osaka Metro 谷町線",
|
||||
"Osaka Metro 堺筋線",
|
||||
|
||||
// 私鉄大手
|
||||
"近鉄奈良線",
|
||||
"近鉄大阪線",
|
||||
"京阪本線",
|
||||
"阪急京都線",
|
||||
"阪急神戸線",
|
||||
"阪急宝塚線",
|
||||
"阪神本線",
|
||||
"阪神なんば線"
|
||||
)
|
||||
|
||||
// -------------------------
|
||||
// 中国
|
||||
// -------------------------
|
||||
"chugoku" -> listOf(
|
||||
"JR西日本(山陽本線)",
|
||||
"JR西日本(可部線)",
|
||||
"JR西日本(伯備線)",
|
||||
"広島電鉄(市内線)",
|
||||
"岡山電気軌道",
|
||||
"一畑電車"
|
||||
)
|
||||
|
||||
|
||||
// -------------------------
|
||||
// 四国
|
||||
// -------------------------
|
||||
"shikoku" -> listOf(
|
||||
"JR四国(予讃線)",
|
||||
"JR四国(高徳線)",
|
||||
"JR四国(徳島線)",
|
||||
"JR四国(土讃線)"
|
||||
)
|
||||
|
||||
// -------------------------
|
||||
// 九州
|
||||
// -------------------------
|
||||
"kyushu" -> listOf(
|
||||
"JR九州(鹿児島本線)",
|
||||
"JR九州(長崎本線)",
|
||||
"JR九州(日豊本線)",
|
||||
"JR九州(久大本線)",
|
||||
"福岡市地下鉄 空港線",
|
||||
"福岡市地下鉄 箱崎線",
|
||||
"福岡市地下鉄 七隈線",
|
||||
"西鉄天神大牟田線"
|
||||
)
|
||||
|
||||
else -> listOf("該当する路線がありません")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,15 @@ object FollowSettings {
|
||||
val prefs = context.getSharedPreferences("follow", Context.MODE_PRIVATE)
|
||||
prefs.edit().putStringSet("lines", lines.toSet()).apply()
|
||||
}
|
||||
|
||||
fun loadNotifySettings(context: Context): Set<String> {
|
||||
val prefs = context.getSharedPreferences("follow", Context.MODE_PRIVATE)
|
||||
return prefs.getStringSet("notify", emptySet()) ?: emptySet()
|
||||
}
|
||||
|
||||
fun saveNotifySettings(context: Context, types: Set<String>) {
|
||||
val prefs = context.getSharedPreferences("follow", Context.MODE_PRIVATE)
|
||||
prefs.edit().putStringSet("notify", types).apply()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ class HiyoriResponder : CharacterResponder {
|
||||
"気になったことがあるの?よかったら教えてね。いっしょに見てみるよ。"
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(text: String): String = respond(text, InfoType.NORMAL)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ enum class InfoType {
|
||||
EVENT, // イベント・臨時列車
|
||||
WEATHER, // 天候影響
|
||||
NEW_TRAIN, // 新型車両関連
|
||||
OTHER // 分類できない場合
|
||||
OTHER, // 分類できない場合
|
||||
|
||||
NORMAL
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
import android.content.Intent
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
|
||||
class LinePriorityActivity : ComponentActivity() {
|
||||
|
||||
@@ -34,6 +35,20 @@ class LinePriorityActivity : ComponentActivity() {
|
||||
|
||||
val boxes = listOf(box1, box2, box3, box4, box5, box6)
|
||||
|
||||
val saved = FollowSettings.loadNotifySettings(this)
|
||||
|
||||
val cbDelay = findViewById<CheckBox>(R.id.checkboxDelay)
|
||||
val cbOuting = findViewById<CheckBox>(R.id.checkboxOuting)
|
||||
val cbCampaign = findViewById<CheckBox>(R.id.checkboxCampaign)
|
||||
val cbOther = findViewById<CheckBox>(R.id.checkboxOther)
|
||||
|
||||
// 保存済みの状態を反映
|
||||
cbDelay.isChecked = saved.contains("delay")
|
||||
cbOuting.isChecked = saved.contains("outing")
|
||||
cbCampaign.isChecked = saved.contains("campaign")
|
||||
cbOther.isChecked = saved.contains("other")
|
||||
|
||||
|
||||
// ▼▼ 空き枠を探して路線名をセット ▼▼
|
||||
for (box in boxes) {
|
||||
val t = box.text.toString()
|
||||
@@ -49,4 +64,25 @@ class LinePriorityActivity : ComponentActivity() {
|
||||
}
|
||||
// ▲▲ 空き枠を探してセット ▲▲
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
val selected = mutableSetOf<String>()
|
||||
|
||||
if (findViewById<CheckBox>(R.id.checkboxDelay).isChecked)
|
||||
selected.add("delay")
|
||||
|
||||
if (findViewById<CheckBox>(R.id.checkboxOuting).isChecked)
|
||||
selected.add("outing")
|
||||
|
||||
if (findViewById<CheckBox>(R.id.checkboxCampaign).isChecked)
|
||||
selected.add("campaign")
|
||||
|
||||
if (findViewById<CheckBox>(R.id.checkboxOther).isChecked)
|
||||
selected.add("other")
|
||||
|
||||
FollowSettings.saveNotifySettings(this, selected)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.example.curation_train_app
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -13,6 +14,8 @@ class LineSelectActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_line_select)
|
||||
|
||||
|
||||
|
||||
// ★ 地域名を受け取る
|
||||
val region = intent.getStringExtra("region") ?: "未指定"
|
||||
|
||||
@@ -31,6 +34,19 @@ class LineSelectActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLineSelected(id: String) {
|
||||
val lines = FollowSettings.loadLines(this).toMutableList()
|
||||
|
||||
if (!lines.contains(id)) {
|
||||
lines.add(id)
|
||||
FollowSettings.saveLines(this, lines)
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 優先度設定画面へ遷移
|
||||
private fun openPriorityScreen(line: String) {
|
||||
val intent = Intent(this, LinePriorityActivity::class.java)
|
||||
|
||||
@@ -15,126 +15,40 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.Call
|
||||
import org.json.JSONObject
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private var lastTrafficStatus: String = ""
|
||||
private var rssList: List<NewsItem> = emptyList()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// --- キャラ初期画像 ---
|
||||
val charId = CharacterSettings.loadCharacter(this)
|
||||
val imageView = findViewById<ImageView>(R.id.imageCharacter)
|
||||
setInitialCharacterImage()
|
||||
|
||||
val drawableId = when (charId) {
|
||||
"reimu" -> R.drawable.reimu_normal
|
||||
"marisa" -> R.drawable.marisa_normal
|
||||
"flandre" -> R.drawable.flandre_normal
|
||||
"sanae" -> R.drawable.sanae_normal
|
||||
"akane" -> R.drawable.akane_normal
|
||||
"momoka" -> R.drawable.momoka_normal
|
||||
"sayaka" -> R.drawable.sayaka_normal
|
||||
"hiyori" -> R.drawable.hiyori_normal
|
||||
else -> R.drawable.reimu_normal
|
||||
}
|
||||
imageView.setImageResource(drawableId)
|
||||
|
||||
// ----- キャラ応答ボタン -----
|
||||
val btn = findViewById<Button>(R.id.btnTestReply)
|
||||
val inputText = findViewById<EditText>(R.id.inputText)
|
||||
val commentView = findViewById<TextView>(R.id.textCharacterComment)
|
||||
|
||||
btn.setOnClickListener {
|
||||
val text = inputText.text.toString()
|
||||
val charId = CharacterSettings.loadCharacter(this)
|
||||
val infoType = InfoClassifier.classify(text)
|
||||
|
||||
Thread {
|
||||
val result = CharacterReplyManager().replyWithAI(charId, text, infoType)
|
||||
runOnUiThread {
|
||||
commentView.text = result.text
|
||||
updateCharacterExpression(charId, result.emotion)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
// --- 運行情報 & RSS 読み込み ---
|
||||
// --- 運行情報 & RSS ---
|
||||
loadTraffic()
|
||||
loadRss()
|
||||
|
||||
// --- RecyclerView 初期設定 ---
|
||||
// RecyclerView
|
||||
val recyclerNews = findViewById<RecyclerView>(R.id.recyclerNews)
|
||||
recyclerNews.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
// 路線設定画面へ遷移
|
||||
val btnOpenRegion = findViewById<TextView>(R.id.textFavorite)
|
||||
btnOpenRegion.setOnClickListener {
|
||||
val intent = Intent(this, RegionSelectActivity::class.java)
|
||||
startActivity(intent)
|
||||
// 路線設定
|
||||
findViewById<TextView>(R.id.textFavorite).setOnClickListener {
|
||||
startActivity(Intent(this, RegionSelectActivity::class.java))
|
||||
}
|
||||
|
||||
applyCharacterLayoutDefault()
|
||||
|
||||
// 詳細画面へ遷移
|
||||
val titleView = findViewById<TextView>(R.id.textTitle)
|
||||
titleView.setOnClickListener {
|
||||
val text = titleView.tag as? String ?: "(詳細情報なし)"
|
||||
val intent = Intent(this, TrafficDetailActivity::class.java)
|
||||
intent.putExtra("text", text)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
|
||||
private fun applyCharacterLayoutDefault() {
|
||||
val img = findViewById<ImageView>(R.id.imageCharacter)
|
||||
val scale = resources.displayMetrics.density
|
||||
|
||||
val widthDp = 110
|
||||
val heightDp = 110
|
||||
val offsetXdp = -8
|
||||
val offsetYdp = -18
|
||||
|
||||
img.layoutParams.width = (widthDp * scale).toInt()
|
||||
img.layoutParams.height = (heightDp * scale).toInt()
|
||||
|
||||
val lp = img.layoutParams as ViewGroup.MarginLayoutParams
|
||||
lp.marginStart = (offsetXdp * scale).toInt()
|
||||
lp.topMargin = (offsetYdp * scale).toInt()
|
||||
|
||||
img.layoutParams = lp
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
|
||||
private fun loadRss() {
|
||||
Thread {
|
||||
try {
|
||||
val api = RssApi()
|
||||
val feed = api.loadRss()
|
||||
|
||||
val list = feed?.channel?.items?.map { item ->
|
||||
NewsItem(
|
||||
company = "JR西日本",
|
||||
title = item.title ?: "",
|
||||
body = item.description ?: ""
|
||||
)
|
||||
} ?: emptyList()
|
||||
|
||||
runOnUiThread {
|
||||
val recyclerNews = findViewById<RecyclerView>(R.id.recyclerNews)
|
||||
recyclerNews.adapter = NewsAdapter(list)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
|
||||
// =========================================================
|
||||
// 運行情報ロード
|
||||
// =========================================================
|
||||
private fun loadTraffic() {
|
||||
Thread {
|
||||
try {
|
||||
@@ -144,27 +58,19 @@ class MainActivity : ComponentActivity() {
|
||||
.build()
|
||||
|
||||
val client = retrofit.create(HtmlClient::class.java)
|
||||
val response = client.getHtml().execute()
|
||||
val html = response.body()?.string() ?: return@Thread
|
||||
|
||||
val html = client.getHtml().execute().body()?.string() ?: return@Thread
|
||||
val doc = Jsoup.parse(html)
|
||||
val lines = doc.select("div.jisyo")
|
||||
|
||||
var hasDelay = false
|
||||
var hasSuspension = false
|
||||
var hasInfo = false
|
||||
|
||||
val items = mutableListOf<NewsItem>()
|
||||
|
||||
val lines = doc.select("div.jisyo")
|
||||
for (line in lines) {
|
||||
val title = line.select("h2").text()
|
||||
val body = line.select("p.gaiyo").text()
|
||||
val classes = line.classNames()
|
||||
|
||||
if (classes.contains("stop")) hasSuspension = true
|
||||
if (classes.contains("delay")) hasDelay = true
|
||||
if (classes.contains("info")) hasInfo = true
|
||||
|
||||
val c = line.classNames()
|
||||
if (c.contains("stop")) hasSuspension = true
|
||||
if (c.contains("delay")) hasDelay = true
|
||||
if (c.contains("info")) hasInfo = true
|
||||
}
|
||||
|
||||
val statusText = when {
|
||||
@@ -174,11 +80,28 @@ class MainActivity : ComponentActivity() {
|
||||
else -> "平常運転"
|
||||
}
|
||||
|
||||
lastTrafficStatus = statusText
|
||||
val infoText = "現在の運行状況:$statusText"
|
||||
|
||||
// UI 反映
|
||||
runOnUiThread {
|
||||
val statusView = findViewById<TextView>(R.id.trafficStatus)
|
||||
statusView.text = "設定した路線:$statusText"
|
||||
|
||||
|
||||
findViewById<TextView>(R.id.trafficStatus).text =
|
||||
"設定した路線:$statusText"
|
||||
}
|
||||
|
||||
// ===== 平常時は RSS の情報からコメント生成 =====
|
||||
if (lastTrafficStatus == "平常運転") {
|
||||
if (rssList.isNotEmpty()) {
|
||||
val importantItem = pickImportantRssItem(rssList)
|
||||
|
||||
if (importantItem != null) {
|
||||
val infoText = "${importantItem.title}\n${importantItem.body}"
|
||||
generateAutoCommentFromInfo(infoText)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 遅延・見合わせは優先
|
||||
generateAutoCommentFromInfo(infoText)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -187,33 +110,42 @@ class MainActivity : ComponentActivity() {
|
||||
}.start()
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// =========================================================
|
||||
// RSSロード
|
||||
// =========================================================
|
||||
private fun loadRss() {
|
||||
Thread {
|
||||
try {
|
||||
val api = RssApi()
|
||||
val feed = api.loadRss()
|
||||
|
||||
fun emotionToExpression(type: EmotionType): String {
|
||||
return when (type) {
|
||||
EmotionType.HAPPY -> "happy"
|
||||
EmotionType.ANGRY -> "angry"
|
||||
EmotionType.SLEEPY -> "sleepy"
|
||||
EmotionType.SURPRISED -> "surprised"
|
||||
EmotionType.CONFUSED -> "confused"
|
||||
EmotionType.THINK -> "think"
|
||||
EmotionType.WORRY -> "worry"
|
||||
else -> "normal"
|
||||
}
|
||||
}
|
||||
rssList = feed?.channel?.items?.map { item ->
|
||||
NewsItem(
|
||||
company = "JR西日本",
|
||||
title = item.title ?: "",
|
||||
body = item.description ?: ""
|
||||
)
|
||||
} ?: emptyList()
|
||||
|
||||
fun updateCharacterExpression(charId: String, type: EmotionType) {
|
||||
val imageView = findViewById<ImageView>(R.id.imageCharacter)
|
||||
val expression = emotionToExpression(type)
|
||||
val resName = "${charId}_${expression}"
|
||||
val resId = resources.getIdentifier(resName, "drawable", packageName)
|
||||
runOnUiThread {
|
||||
val recycler = findViewById<RecyclerView>(R.id.recyclerNews)
|
||||
recycler.adapter = NewsAdapter(rssList)
|
||||
}
|
||||
|
||||
if (resId != 0)
|
||||
imageView.setImageResource(resId)
|
||||
else {
|
||||
val normalId = resources.getIdentifier("${charId}_normal", "drawable", packageName)
|
||||
imageView.setImageResource(normalId)
|
||||
}
|
||||
// 平常運転のとき → RSSの重要情報でコメント生成
|
||||
if (lastTrafficStatus == "平常運転" && rssList.isNotEmpty()) {
|
||||
val importantItem = pickImportantRssItem(rssList)
|
||||
|
||||
if (importantItem != null) {
|
||||
val infoText = "${importantItem.title}\n${importantItem.body}"
|
||||
generateAutoCommentFromInfo(infoText)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -235,7 +167,123 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
imageView.setImageResource(drawableId)
|
||||
|
||||
// ---- キャラ切り替え後、自動で最新コメントを再生成 ----
|
||||
// ---- キャラ切り替え後、自動で最新コメントを再生成 ----
|
||||
if (lastTrafficStatus == "平常運転" && rssList.isNotEmpty()) {
|
||||
|
||||
// ★ここを変更(ランダム or 後でフィルタできるように)
|
||||
val item = pickRandomRssItem(rssList)
|
||||
|
||||
if (item != null) {
|
||||
val infoText = "${item.title}\n${item.body}"
|
||||
generateAutoCommentFromInfo(infoText)
|
||||
}
|
||||
|
||||
} else {
|
||||
// 遅延・運転見合わせならその情報を優先
|
||||
val infoText = "現在の運行状況: $lastTrafficStatus"
|
||||
generateAutoCommentFromInfo(infoText)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun classifyRssType(title: String, body: String): String {
|
||||
return when {
|
||||
title.contains("遅れ") || title.contains("運転") -> "delay"
|
||||
title.contains("イベント") || body.contains("イベント") -> "outing"
|
||||
title.contains("キャンペーン") -> "campaign"
|
||||
else -> "other"
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExpressionByKeyword(text: String) {
|
||||
val imageView = findViewById<ImageView>(R.id.imageCharacter)
|
||||
val charId = CharacterSettings.loadCharacter(this)
|
||||
|
||||
val isDelay = text.contains("遅延") || text.contains("遅れ")
|
||||
val isStop = text.contains("運転見合わせ") || text.contains("運休")
|
||||
|
||||
val expression = when {
|
||||
isStop -> "angry" // 運転見合わせ=怒り
|
||||
isDelay -> "worry" // 遅延=困り顔
|
||||
else -> "normal"
|
||||
}
|
||||
|
||||
val resId = resources.getIdentifier("${charId}_${expression}", "drawable", packageName)
|
||||
imageView.setImageResource(resId)
|
||||
}
|
||||
|
||||
private fun pickImportantRssItem(list: List<NewsItem>): NewsItem? {
|
||||
if (list.isEmpty()) return null
|
||||
|
||||
val keywords = listOf("工事", "運休", "変更", "改良", "新設", "増発", "減便")
|
||||
|
||||
// ① 重要記事を抽出
|
||||
val important = list.filter { item ->
|
||||
keywords.any { kw ->
|
||||
item.title.contains(kw) || item.body.contains(kw)
|
||||
}
|
||||
}
|
||||
|
||||
// ② 重要記事が複数 → ランダムに1件
|
||||
if (important.isNotEmpty()) {
|
||||
return important.random()
|
||||
}
|
||||
|
||||
// ③ 重要記事ゼロ → 全RSSからランダム
|
||||
return list.random()
|
||||
}
|
||||
|
||||
private fun generateAutoCommentFromInfo(text: String) {
|
||||
Thread {
|
||||
try {
|
||||
val charId = CharacterSettings.loadCharacter(this)
|
||||
val infoType = InfoClassifier.classify(text)
|
||||
|
||||
val json = CharacterReplyManager().replyWithAI(
|
||||
characterId = charId,
|
||||
info = text,
|
||||
type = infoType
|
||||
)
|
||||
|
||||
val clean = AiReplyParser.extractTextFromResponse(json)
|
||||
|
||||
runOnUiThread {
|
||||
findViewById<TextView>(R.id.textAutoComment).text = clean
|
||||
}
|
||||
|
||||
} catch (e: Exception) { e.printStackTrace() }
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun setInitialCharacterImage() {
|
||||
val charId = CharacterSettings.loadCharacter(this)
|
||||
val imageView = findViewById<ImageView>(R.id.imageCharacter)
|
||||
val drawable = resources.getIdentifier("${charId}_smile", "drawable", packageName)
|
||||
imageView.setImageResource(drawable)
|
||||
}
|
||||
|
||||
private fun applyCharacterLayoutDefault() {
|
||||
val img = findViewById<ImageView>(R.id.imageCharacter)
|
||||
val scale = resources.displayMetrics.density
|
||||
val lp = img.layoutParams as ViewGroup.MarginLayoutParams
|
||||
lp.marginStart = (-8 * scale).toInt()
|
||||
lp.topMargin = (-18 * scale).toInt()
|
||||
img.layoutParams = lp
|
||||
}
|
||||
|
||||
private fun pickRandomRssItem(list: List<NewsItem>): NewsItem? {
|
||||
if (list.isEmpty()) return null
|
||||
return list.random() // ← ランダム取得
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// =========================================================
|
||||
// コメント生成
|
||||
// =========================================================
|
||||
|
||||
|
||||
|
||||
@@ -20,4 +20,7 @@ class MarisaResponder : CharacterResponder {
|
||||
"ふむ?何か気になることがあるんだな。他にも情報があれば教えてくれよ。"
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(text: String): String = respond(text, InfoType.NORMAL)
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ class MomokaResponder : CharacterResponder{
|
||||
"気になることがあったんだね。よかったら、もう少し教えてねっ!"
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(text: String): String = respond(text, InfoType.NORMAL)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.curation_train_app
|
||||
|
||||
data class RailLine(
|
||||
val id: String, // "hanwa"
|
||||
val name: String, // "阪和線"
|
||||
val company: String // "jr_west"
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.curation_train_app
|
||||
|
||||
object RailLineData {
|
||||
val lines = listOf(
|
||||
RailLine("hanwa", "阪和線", "jr_west"),
|
||||
RailLine("kyoto", "JR京都線", "jr_west"),
|
||||
RailLine("kobes", "JR神戸線", "jr_west"),
|
||||
RailLine("takarazuka", "JR宝塚線", "jr_west"),
|
||||
|
||||
// 南海
|
||||
RailLine("nankai_main", "南海本線", "nankai"),
|
||||
RailLine("koya", "高野線", "nankai"),
|
||||
RailLine("senboku", "泉北高速線", "senboku"),
|
||||
|
||||
// ここにどんどん追加できる
|
||||
)
|
||||
}
|
||||
@@ -20,4 +20,8 @@ class ReimuResponder : CharacterResponder {
|
||||
"気になったのね。内容をもう少し教えてくれれば、ちゃんと見てみるわよ。"
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(text: String): String = respond(text, InfoType.NORMAL)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,7 @@ class SanaeResponder : CharacterResponder {
|
||||
"気になる点がありましたか?よければ詳しく教えてくださいね。"
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(text: String): String = respond(text, InfoType.NORMAL)
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,7 @@ class SayakResponder : CharacterResponder {
|
||||
"気になる点があるのね。もう少し詳しく教えてくれれば、整理してお話しできると思うわ。"
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(text: String): String = respond(text, InfoType.NORMAL)
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ class flandreResponder : CharacterResponder {
|
||||
"なになに?気になることがあるの?フランに教えてほしいのだ!"
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(text: String): String = respond(text, InfoType.NORMAL)
|
||||
}
|
||||
|
||||
10
app/src/main/res/drawable/bg_comment_bubble.xml
Normal file
10
app/src/main/res/drawable/bg_comment_bubble.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#FFFFFF"/>
|
||||
<corners android:radius="12dp"/>
|
||||
<padding
|
||||
android:left="8dp"
|
||||
android:top="8dp"
|
||||
android:right="8dp"
|
||||
android:bottom="8dp"/>
|
||||
<stroke android:width="1dp" android:color="#DDDDDD"/>
|
||||
</shape>
|
||||
@@ -97,4 +97,44 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutNotify"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="お知らせ設定"
|
||||
android:textStyle="bold"
|
||||
android:padding="4dp"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkboxDelay"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="運行・遅延情報"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkboxOuting"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="お出かけ関連情報"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkboxCampaign"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="キャンペーン・お楽しみ情報"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkboxOther"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="その他"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</ScrollView>
|
||||
|
||||
@@ -129,77 +129,42 @@
|
||||
android:padding="8dp" />
|
||||
|
||||
|
||||
<!-- ▼▼ キャラ応答テストUI ▼▼ -->
|
||||
<LinearLayout
|
||||
android:id="@+id/testArea"
|
||||
android:id="@+id/commentArea"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="#EEEEEE">
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputText"
|
||||
android:layout_width="match_parent"
|
||||
<!-- 吹き出し(白枠) -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="テスト用の文章を入力" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputCharacter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="キャラID(例:reimu / akane)" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnTestReply"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="キャラにしゃべらせる" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textTestResult"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="※結果がここに表示されます"
|
||||
android:paddingTop="8dp" />
|
||||
|
||||
<!-- ▼▼ キャラ表示エリア(正しい構造) ▼▼ -->
|
||||
<FrameLayout
|
||||
android:id="@+id/bottomArea"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="180dp"
|
||||
android:background="@drawable/bg_comment_bubble"
|
||||
android:padding="12dp">
|
||||
|
||||
<!-- 左側のセリフ枠 -->
|
||||
<TextView
|
||||
android:id="@+id/textCharacterComment"
|
||||
android:layout_width="250dp"
|
||||
android:id="@+id/textAutoComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/comment_background_white"
|
||||
android:padding="13dp"
|
||||
android:text="ちょっとした一言"
|
||||
android:textSize="13sp"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:translationX="0dp"
|
||||
android:translationY="0dp" />
|
||||
android:textSize="15sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 右下のキャラ画像 -->
|
||||
<ImageView
|
||||
android:id="@+id/imageCharacter"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="250dp"
|
||||
android:adjustViewBounds="false"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/akane_01_icon"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="8dp"
|
||||
android:cropToPadding="false"
|
||||
/>
|
||||
<!-- キャラ画像(右寄せ) -->
|
||||
<ImageView
|
||||
android:id="@+id/imageCharacter"
|
||||
android:layout_width="110dp"
|
||||
android:layout_height="110dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_gravity="bottom" />
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- ▼▼ キャラ応答テストUI ▼▼ -->
|
||||
<!-- ▲▲ キャラ表示エリア ▲▲ -->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ▲▲ キャラ応答テストUI ▲▲ -->
|
||||
|
||||
<!-- ▲▲ キャラ応答テストUI ▲▲ -->
|
||||
|
||||
@@ -119,56 +119,56 @@
|
||||
android:id="@+id/btnReimu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🔴 霊夢(キャラ紹介)"
|
||||
android:text="🔴 霊夢(落ち着いた案内役)"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnMarisa"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🟡 魔理沙(キャラ紹介)"
|
||||
android:text="🟡 魔理沙(専門的な深掘り担当)"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnFrandle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🟠 フラン(キャラ紹介)"
|
||||
android:text="🟠 フラン(明るい元気ムードメーカー)"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSanae"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🟢 早苗(キャラ紹介)"
|
||||
android:text="🟢 早苗(専門的な深掘り担当)"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnAkane"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🟨 あかね(キャラ紹介)"
|
||||
android:text="🟨 あかね(素直なリアクション枠)"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnMomoka"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🍑 ももか(キャラ紹介)"
|
||||
android:text="🍑 ももか(素直なリアクション枠)"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSayaka"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🟦 さやか(キャラ紹介)"
|
||||
android:text="🟦 さやか(穏やかな整理・補足役)"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnHiyori"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🟧 ひより(キャラ紹介)"
|
||||
android:text="🟧 ひより(柔らかい反応の癒し枠)"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
Reference in New Issue
Block a user