24aw0123のアイコン画像
24aw0123 2025年01月31日作成
製作品 製作品 閲覧数 187
24aw0123 2025年01月31日作成 製作品 製作品 閲覧数 187

いつのまにか小説

いつのまにか小説

はじめに

こんにちは。私たちは日本電子専門学校の学生です。
今回、学年も専門分野も違う私たちが目指したものは、「私たちにとって、暮らしやすい社会」でした。
未来、私たちにとって暮らしやすい社会を考える際に、スタートとともに考えたのは、「未来の自分がどうありたいか?」という「あるべき姿」。
10年、20年先の未来も、自分たちが自分らしく楽しく生き続けられるような気持ちを大切にして制作しました。

テーマ:「旅とは」

今回チームで着目したのは「旅」。

私たちにとって、「旅」とは?
旅とは・・・
忙しい日常を離れ、心身をリフレッシュするためだったり、
歴史的な名所や博物館を訪れ、その土地の文化や歴史を学ぶため・・・
はたまた、普段の生活ではできない冒険を楽しむことだっだり、
自然や建築の壮大さに触れて感動できたり・・・。

共通しているのは「ひと時だけ日常を離れて新しい何かを得る」ということだと見出し、
旅を通して、個々の人生をより豊かにするためのサービスを企画しました。

コンセプト:「出会いと別れを経験しながら、自分の変化に気づく」

私たちにとっての旅とは、出会いと別れを経験しながら自分の変化に気付くことでした。

日常を離れて新しい何かを得るということは、人生の中で非常に大きな意味を持つ出来事であり、感情や人間関係に深い影響を与えるものです。
この経験は私たちの成長や自己理解、そして人間関係の築き方において重要な役割を果たします。

私たちは、学校や職場などで、多くの出会いと別れを経験してきました。
「出会い」は、その人の考え方、価値観、文化に触れ、自分の視野が広がります。
「別れ」は、悲しい試練となりますが、感情を整理し、人間関係に対する考え方が変わることがあります。
出会いを大切にし、別れを受け入れることで、人間として大きく成長できるのでは?と感じ、出会いと別れを経験しながら、自分の変化に気付く旅サービス「いつのまにか小説」を提案します。

イメージ「旅」×「小説」

旅をしながらその体験を記録することで、後で振り返ったときに思い出が蘇ったり、自分自身と向き合えることができるサービスを提案します。

つまり、出会いと別れを記録し、「小説」をつくりあげます。
旅先で出会った人や景色、その時感じたこと、印象に残った言葉を残すことができます。
旅の途中で出会った景色や人との別れが、どのように自分に影響を与えたのか、感情の変化を「小説」で振り返ることができるサービス内容です。

出会いと別れの記録が「小説」となり、後で読み返したり思い出したりすることで、旅の意味をさらに深めることができます。

ターゲットユーザー

サービスを利用する主なターゲット層は、環境や人間関係が大きく変化する10~20代の本好きな人です。

サービスの構成

サービスの「いつのまにか小説」は、下記の2つで構成されています。
①アプリ:しおり文庫
②デバイス(カメラ):novel

①しおり文庫(アプリ)
キャプションを入力できます

②novel(デバイス:カメラ)
キャプションを入力できます

「いつのまにか小説」の主な機能

① 主人公と登場人物を設定し、カメラデバイス「novel」を使って写真を撮ると小説が自動生成されます
② 撮影と同時に録音し、小説に感情を盛り込むことができます
③ 写真を撮ったところの位置情報等からAIが環境を読み取り、小説に反映させることができます
④ 書いた小説は、旅で訪れた場所の近くに行くことで読み返すことができます

「いつのまにか小説」のシナリオ

「いつのまにか小説」をご利用いただくときのシナリオを動画にしました。


キャプションを入力できます

https://youtu.be/ZpnyYP4j0Bc

①小説の主人公を設定する
主人公の名前、年齢、性別を設定する。(必須)
友人など一緒に旅行に行く人の設定も可能
キャプションを入力できます

②旅に出る
自分の行きたいところへ旅に出る
キャプションを入力できます

③写真を撮る
旅の中で心が動いた場所の景色を撮ったり、友人と一緒に写真を撮ったりする
キャプションを入力できます

④自動で小説が生成される
設定した情報や声、周囲の状況等をAIが読み取り、小説を生成する。
キャプションを入力できます

⑤小説を読み返す
書いた小説は、旅をした場所の近くに行くと読み返すことができる。
読み返すことで変化を発見したり、また旅に出るきっかけになる。
ランダムで小説のタイトルが表示され、タップするとその小説を読むことができる。
キャプションを入力できます

日付順に新しいものから読むことができる。
キャプションを入力できます

ハンバーガーメニューで読む・作るページ間の遷移ができる。
キャプションを入力できます

部品

名前 URL
SPRESENSE 拡張ボード https://00m.in/UlaLO
SONY SPRESENSE メインボード https://00m.in/SMVmL
SPRESENSE HDRカメラボード https://00m.in/EvZBO
SPRESENSE Wi-Fi Add-onボード https://00m.in/NenQT

キャプションを入力できます

キャプションを入力できます

データフロー

データフローは以下の写真のとおりです。

キャプションを入力できます

ソースコード

wifiに接続後novel(カメラ)を起動し、撮影します。
撮影した画像をSDカードに保存後、WEBサーバーにPOSTします。

wifiCamera.ino

#include <TelitWiFi.h> #include "config.h" #include <Arduino.h> #include <File.h> #include <SDHCI.h> #include <Camera.h> #include <stdio.h> /* for sprintf */ #define CONSOLE_BAUDRATE 115200 SDClass SD; /**< SDClass object */ File myFile; /**< File object */ TelitWiFi gs2200; TWIFI_Params gsparams; int take_picture_count = 0; const char* uploadFilePath = "PICT000.JPG"; void printError(enum CamErr err) { Serial.print("Error: "); switch (err) { case CAM_ERR_NO_DEVICE: Serial.println("No Device"); break; case CAM_ERR_ILLEGAL_DEVERR: Serial.println("Illegal device error"); break; case CAM_ERR_ALREADY_INITIALIZED: Serial.println("Already initialized"); break; case CAM_ERR_NOT_INITIALIZED: Serial.println("Not initialized"); break; case CAM_ERR_NOT_STILL_INITIALIZED: Serial.println("Still picture not initialized"); break; case CAM_ERR_CANT_CREATE_THREAD: Serial.println("Failed to create thread"); break; case CAM_ERR_INVALID_PARAM: Serial.println("Invalid parameter"); break; case CAM_ERR_NO_MEMORY: Serial.println("No memory"); break; case CAM_ERR_USR_INUSED: Serial.println("Buffer already in use"); break; case CAM_ERR_NOT_PERMITTED: Serial.println("Operation not permitted"); break; default: break; } } /** * Callback from Camera library when video frame is captured. */ void CamCB(CamImage img) { if (img.isAvailable()) { img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); Serial.print("Image data size = "); Serial.print(img.getImgSize(), DEC); Serial.print(" , "); Serial.print("buff addr = "); Serial.print((unsigned long)img.getImgBuff(), HEX); Serial.println(""); } else { Serial.println("Failed to get video stream image"); } } void setup() { Serial.begin(CONSOLE_BAUDRATE); while (!Serial) { ; // wait for serial port to connect. } Serial.print("Insert SD card."); while (!SD.begin()) { Serial.println("SD card initialization failed. Please check the card."); delay(1000); } myFile = SD.open(uploadFilePath, FILE_WRITE); pinMode(LED0, OUTPUT); digitalWrite(LED0, LOW); // LEDを消す Init_GS2200_SPI_type(iS110B_TypeC); gsparams.mode = ATCMD_MODE_STATION; gsparams.psave = ATCMD_PSAVE_DEFAULT; if (gs2200.begin(gsparams)) { Serial.println("GS2200の初期化に失敗しました"); while (1); } CamErr err; Serial.println("Prepare camera"); err = theCamera.begin(); if (err != CAM_ERR_SUCCESS) { printError(err); return; // 初期化失敗時は終了 } Serial.println("Start streaming"); err = theCamera.startStreaming(true, CamCB); if (err != CAM_ERR_SUCCESS) { printError(err); return; // ストリーミング失敗時は終了 } Serial.println("Set Auto white balance parameter"); err = theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT); if (err != CAM_ERR_SUCCESS) { printError(err); } Serial.println("Set still picture format"); err = theCamera.setStillPictureImageFormat( CAM_IMGSIZE_VGA_H, // VGAの幅 CAM_IMGSIZE_VGA_V, // VGAの高さ CAM_IMAGE_PIX_FMT_JPG); if (err != CAM_ERR_SUCCESS) { printError(err); } // ストリーミングの初期化を待つ delay(1000); // 1秒待つ // GS2200をAPに接続する connectToAP(); } void connectToAP() { while (true) { if (gs2200.activate_station(AP_SSID, PASSPHRASE)) { Serial.println("APへの接続に失敗しました。再試行します..."); digitalWrite(LED0, LOW); // LEDを消す delay(2000); // 2秒待機して再試行 } else { Serial.println("APに接続しました"); digitalWrite(LED0, HIGH); // LEDを点灯 if (take_picture_count < 1) // 1枚だけ撮影 { Serial.println("call takePicture()"); CamImage img = theCamera.takePicture(); if (img.isAvailable()) { char filename[16] = {0}; sprintf(filename, "PICT%03d.JPG", take_picture_count); Serial.print("Save taken picture as "); Serial.print(filename); Serial.println(""); SD.remove(filename); File myFile = SD.open(filename, FILE_WRITE); myFile.write(img.getImgBuff(), img.getImgSize()); myFile.close(); } else { Serial.println("Failed to take picture"); } // 一枚撮影後にカメラを終了 Serial.println("End."); theCamera.end(); } take_picture_count++; if (myFile) { Serial.print("open collect"); myFile.close(); Serial.println("done."); } else { Serial.println("error opening "); } break; // 接続成功したのでループを抜ける } } } void loop() { // ループ内では特に処理を行わない delay(1000); // 1秒待機 }

oconfig.h

#ifndef _CONFIG_H_ #define _CONFIG_H_ /*-------------------------------------------------------------------------* * Configration *-------------------------------------------------------------------------*/ #define AP_SSID "SSID" #define PASSPHRASE "PASS" // #define HTTP_SRVR_IP "192.168.1.100" // #define HTTP_PORT "10080" // #define HTTP_GET_PATH "/" // #define HTTP_POST_PATH "/postData" #endif /*_CONFIG_H_*/

Laravel

<?php use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use App\Http\Controllers\UserController; use App\Http\Controllers\MemoryController; use App\Http\Controllers\GeocodingController; use App\Http\Controllers\WeatherController; use App\Http\Controllers\LoginController; use App\Http\Controllers\ImageController; use App\Http\Controllers\VoiceController; Route::get('/user',[UserController::class,'index']); Route::get('/regeo',[GeocodingController::class,'show']); Route::get('/weather',[WeatherController::class,'show']); Route::post('/voice',[VoiceController::class,'show']); Route::post('/image',[ImageController::class,'store']); Route::post('/memory',[MemoryController::class,'store']);

GeocodingController

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; use App\Http\Requests\GeocodingRequest; class GeocodingController extends Controller { /** * Display a listing of the resource. */ public function index() { } /** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request) { // } /** * Display the specified resource. */ public function show(GeocodingRequest $request) { $url = "https://map.yahooapis.jp/geoapi/V1/reverseGeoCoder"; $params = [ 'appid' => "app-key", 'lat' => $request->lat, 'lon' => $request->lon, 'output' => "json" ]; $response = Http::get($url,$params); if ($response->successful()) { return response($response,200); }else{ return response([ 'status' => "error" ],400); } } /** * Show the form for editing the specified resource. */ public function edit(string $id) { // } /** * Update the specified resource in storage. */ public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. */ public function destroy(string $id) { // } }

ImageController

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests\ImageRequest; use App\Models\Image; class ImageController extends Controller { /** * Display a listing of the resource. */ public function index() { // } /** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request) { $user_id = 1; // とりあえず1番 $file = $request->file('image'); // 'file'はフォームのフィールド名 if (!$file || !$file->isValid()) { return response(['status' => 'error', 'message' => '無効なファイルです。'], 400); } $fileContent = file_get_contents($file->getRealPath()); $base64Image = base64_encode($fileContent); $item = Image::create([ 'user_id' => $user_id, 'image' => $base64Image ]); return response(['status' => 'success'], 201); } /** * Display the specified resource. */ public function show(string $id) { // } /** * Show the form for editing the specified resource. */ public function edit(string $id) { // } /** * Update the specified resource in storage. */ public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. */ public function destroy(string $id) { // } }

LoginController

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use \Hash; use App\Http\Requests\LoginRequest; use App\Models\User; class LoginController extends Controller { /** * 認証処理 */ public function Login(LoginRequest $request) { $req = $request->all(); $user = User::where('account',$req['account'])->first(); if (!$user) { # いない場合はエラーを返す return response(["status" => "failure", "message" => "アカウントまたはパスワードが違います"], 401); } if (! Hash::check($req['password'], $user->password)) { // 一致してなければエラー return response(["status" => "failure", "message" => "アカウントまたはパスワードが違います"], 401); } $user->token = md5(uniqid(rand(), true)); $user->update(); return response([ "status" => "success", "token" => $user->token ], 200); } /** * Display a listing of the resource. */ public function index() { // } /** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request) { // } /** * Display the specified resource. */ public function show(string $id) { // } /** * Show the form for editing the specified resource. */ public function edit(string $id) { // } /** * Update the specified resource in storage. */ public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. */ public function destroy(string $id) { // } }

MemoryController

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\Memory; class MemoryController extends Controller { /** *データをすべて表示 */ public function index() { // } /** * データの登録 */ public function store(Request $request) { $user_id = 1;//とりあえず1番 $req = $request->all(); //DBに登録 $item = Memory::create([ 'user_id' => $user_id, 'image' => $req['image'], 'voice' => $req['voice'], 'lat' => $req['lat'], 'lng' => $req['lng'], ]); return response(['status' => 'succes'],201); } /** * 主キーをもとにデータを一件だけ表示 */ public function show(string $id) { // } /** * Update the specified resource in storage. */ public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. */ public function destroy(string $id) { // } }

UserController

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\User; class UserController extends Controller { /** * Display a listing of the resource. * DBの中身をすべて表示するときのみ使用 */ public function index() { $list = User::get(); return response($list,200); } /** * Store a newly created resource in storage. * データ保存用 */ public function store(Request $request) { // } /** * Display the specified resource. * 一件表示するときの */ public function show(string $id) { // } /** * Update the specified resource in storage. * 更更新用 */ public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. * 削除用 */ public function destroy(string $id) { // } }

VoiceController

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; class VoiceController extends Controller { /** * Display a listing of the resource. */ public function index() { // } /** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request) { $url = "https://acp-api-async.amivoice.com/v1/recognitions"; $params = [ "authorization" => "", "grammarFileNames" => "-a-general", "sentimentAnalysis" => "True", "audioFile" => $request->file ]; $response = Http::get($url,$params); if ($response->successful()) { return response($response,200); }else{ return response([ 'status' => "error" ],400); } } /** * Display the specified resource. */ public function show(Request $request) { } /** * Show the form for editing the specified resource. */ public function edit(string $id) { // } /** * Update the specified resource in storage. */ public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. */ public function destroy(string $id) { // } }

WeatherController

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; use App\Http\Requests\WeatherRequest; class WeatherController extends Controller { /** * Display a listing of the resource. */ public function index() { // } /** * Show the form for creating a new resource. */ public function create() { // } /** * Store a newly created resource in storage. */ public function store(Request $request) { // } /** * Display the specified resource. */ public function show(Request $request) { $url = "https://api.open-meteo.com/v1/forecast"; $params = [ 'latitude' => $request->lat, 'longitude' => $request->lon, 'current' => "temperature_2m", 'current' => "weather_code", 'timezone' => "Asia/Tokyo", 'models' => "jma_seamless" ]; // $response = Http::get($url,$params); $response = Http::get("https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&hourly=temperature_2m&models=jma_seamless"); // print_r($response); if ($response->successful()) { return response($response->body(),200); // dd($response->json()); }else{ return response([ 'status' => "error" ],400); } } /** * Show the form for editing the specified resource. */ public function edit(string $id) { // } /** * Update the specified resource in storage. */ public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. */ public function destroy(string $id) { // } }

GeocodingRequest

<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\ValidationException; class GeocodingRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. * * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> */ public function rules(): array { return [ 'lon' => 'required', 'lat' => 'required' ]; } /** * Handle a failed validation attempt. * * @param \Illuminate\Validation\Validator $validator * @return void * * @throws \Illuminate\Validation\ValidationException */ protected function failedValidation(Validator $validator) { $exception = $validator->getException(); throw new $exception($validator, response([ 'status' => "ParameterError"],400) ); } }

ImageRequest

<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class ImageRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. * * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> */ public function rules(): array { return [ // ]; } }

LoginRequest

<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class LoginRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. * * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> */ public function rules(): array { return [ 'account' => 'required|max:255', 'password' => 'required|max:255' ]; } }

voiceRequest

<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class voiceRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. * * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> */ public function rules(): array { return [ // ]; } }

WeatherRequest

<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\ValidationException; class WeatherRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. * * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> */ public function rules(): array { return [ 'latitude' => 'required', 'longitude' => 'required' ]; } }

Image

<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Image extends Model { use HasFactory; protected $fillable = ['user_id','image']; }

Memory

<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Memory extends Model { use HasFactory; protected $fillable = ['user_id','image','voice','lat','lng']; }

Page

<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Page extends Model { use HasFactory; protected $fillable = [ 'memory_id', 'text_from_voice', 'weather', 'temp', 'wind', 'address', 'text' ]; }

User

<?php namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use HasFactory, Notifiable; /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'name', // 'email', 'password', ]; /** * The attributes that should be hidden for serialization. * * @var array<int, string> */ protected $hidden = [ 'password', 'token', ]; /** * Get the attributes that should be cast. * * @return array<string, string> */ protected function casts(): array { return [ // 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; } }

入力デバイス novel

今回カメラの筐体は3DCGモデリングをし、3Dプリンターで出力を行いました。
まず、CGソフトMAYAでカメラの形を中にスプレッセンスなどの基盤やバッテリーが入るように設計しモデリングを行いました。
その後CHITIBOXで3Dプリントをする際に必要な支柱を作成し、最後にELEGOO Saturnの3Dプリンターで出力を行いました。

生成終了
キャプションを入力できます

洗浄
キャプションを入力できます

硬化中
キャプションを入力できます

最後に

この「いつのまにか小説」は、カメラデバイスnovelを使用し、いつでも簡単に小説を作ることができ、自分で作った小説を読むことで過去に思いを馳せる事ができる、そんなサービスです。
小説が好きな自分たちも使いたいと思えるサービスができたと思っています。今後もブラッシュアップして、「いつのまにか小説」がより魅力的になるよう磨いていきたいです。

~展望~
・POSTリクエストの実装
・筐体のスケルトン化も含めたデザインの工夫
・フロントエンドの完成
・読む本型端末の完成

日本電子専門学校 学科横断プロジェクト Bチーム
・高度情報処理科 二宮
・コンピュータグラフィック科 水野
・Webデザイン科 塩澤

ログインしてコメントを投稿する