Blog

All Blog Posts  |  Next Post  |  Previous Post



A technical look at the TMS AI City Guide summer project

Today

TMS Software Delphi Components tmsaistudio


The TMS AI City Guide project is a Delphi FireMonkey application that combines interactive maps, AI-generated points of interest, GPX route export, and AI-generated speech. It demonstrates a useful pattern for cross-platform Delphi development: keep the application logic platform-independent and isolate native functionality behind small wrapper units.

The project targets Android, iOS, Windows and MacOS. The main application is defined in TMSCityGuide.dpr, while the UI logic is separated into UILogic.pas. Two particularly reusable examples are the file share functionality in uShareFile.pas and MP3 playback in uMP3Play.pas.

Exporting a GPX file through the device share sheet


The map component can export the current route, including its waypoints, to a GPX file using SaveToGPXFile. The resulting file can then be passed to other applications such as mapping, navigation, fitness, or file-management apps.



The form code first creates a temporary filename, exports the route, and calls one platform-independent procedure:
procedure TCityGuide.BtnShareClick(Sender: TObject);
var
  LFileName: string;
  FName: string;
begin
  if UILogic.City <> '' then
    FName := UILogic.City + '.gpx'
  else
    FName := 'mycitytour.gpx';

  LFileName := TPath.Combine(
    TPath.GetTempPath,
    TPath.GetFileName(FName)
  );

  UILogic.ExportToGPX(TMSFNCMaps, LFileName);

  ShareFile(LFileName, 'application/xml', FName);
end;

The important design decision is that the form does not contain Android or iOS code. It only knows that ShareFile accepts a filename, MIME type, and title.

Android implementation

On Android, ShareFile creates an ACTION_SEND intent. The file is exposed through an AndroidX FileProvider, which produces a secure content URI. The URI is attached to the intent as EXTRA_STREAM, and read permission is granted to the receiving application.
LIntent := TJIntent.Create;
LIntent.setAction(TJIntent.JavaClass.ACTION_SEND);
LIntent.setType(StringToJString(AMimeType));

LIntent.putExtra(
  TJIntent.JavaClass.EXTRA_STREAM,
  TJParcelable.Wrap((LUri as ILocalObject).GetObjectID)
);

LIntent.addFlags(
  TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION
);

TAndroidHelper.Activity.startActivity(
  TJIntent.JavaClass.createChooser(
    LIntent,
    StrToJCharSequence(ATitle)
  )
);

The Android project configuration also needs a FileProvider entry in AndroidManifest.template.xml and a deployed provider_paths.xml. The provider path maps the application’s documents directory so that the file can safely be shared without exposing arbitrary filesystem paths.

iOS implementation

On iOS, the same procedure uses UIActivityViewController, the standard share sheet. The local filename is converted to an NSURL and passed as an activity item:
LURL := TNSURL.Wrap(
  TNSURL.OCClass.fileURLWithPath(
    StrToNSStr(AFilePath)
  )
);

LItems := TNSMutableArray.Create;
LItems.addObject((LURL as ILocalObject).GetObjectID);

LActivity := TUIActivityViewController.Wrap(
  TUIActivityViewController.Alloc.initWithActivityItems(
    LItems,
    nil
  )
);

LRootVC.presentViewController(LActivity, True, nil);

The implementation also configures a popover source view and rectangle when required. This is important for iPad compatibility, where an activity controller must have a presentation source.

The result is a single reusable API:
ShareFile(
  '/path/to/citytour.gpx',
  'application/xml',
  'Share city tour'
);
On desktop platforms, the same wrapper falls back to a TSaveDialog. This is a useful cross-platform technique: expose one common interface while using conditional compilation only inside the implementation unit.

Converting AI-generated descriptions into speech


The application also uses TCloudAI to request speech audio from an AI service. When the user selects a place and presses the speak button, the description is sent to the speech service:
procedure TUILogic.Speak(Description: string);
begin
  FCloudAIAudio.Speak(Description, 1, 'nova');
end;
The audio request is asynchronous. During initialization, the audio-enabled TCloudAI instance is connected to an event handler:
FCloudAIAudio := TCloudAI.Create(Self);
FCloudAIAudio.Service := aiOpenAI;
FCloudAIAudio.OnSpeechAudio := SoundReady;

FMP3Player := TStreamMP3Player.Create;
When the response arrives, the event provides the generated audio as a TMemoryStream:
procedure TUILogic.SoundReady(
  Sender: TObject;
  HttpStatusCode: Integer;
  HttpResult: string;
  SoundBuffer: TMemoryStream
);
begin
  FMP3Player.Play(SoundBuffer);
end;
This keeps the network operation and audio playback loosely coupled. TCloudAI is responsible for obtaining the speech, while TStreamMP3Player only needs to know how to play an MP3 stream.

If you want to see this feature in action, check our video: https://www.youtube.com/watch?v=VPMEdQxjSd4

Playing an MP3 stream with FireMonkey

Mobile media frameworks generally expect a filename rather than an arbitrary Delphi stream. The TStreamMP3Player class bridges that gap:

  1. It creates a unique filename using a GUID.
  2. It writes the stream to TPath.GetCachePath.
  3. It assigns that temporary file to TMediaPlayer.
  4. It starts playback.
  5. It destroys the media player before deleting the temporary file.

The public API remains small:
FMP3Player.Play(SoundBuffer);
The essential implementation is:
procedure TStreamMP3Player.Play(AStream: TStream);
var
  LFileName: string;
  LFileStream: TFileStream;
begin
  if not Assigned(AStream) then
    raise EArgumentNilException.Create('AStream');

  ReleaseMedia;

  LFileName := CreateTempMP3FileName;

  AStream.Position := 0;
  LFileStream := TFileStream.Create(
    LFileName,
    fmCreate or fmShareDenyWrite
  );
  try
    LFileStream.CopyFrom(AStream, 0);
  finally
    LFileStream.Free;
  end;

  FTempFileName := LFileName;
  FMediaPlayer.FileName := FTempFileName;
  FMediaPlayer.CurrentTime := 0;
  FMediaPlayer.Play;
end;
One subtle but important detail is the cleanup order. The native media backend may still have an open handle to the MP3 file. Therefore, TMediaPlayer is stopped and destroyed first; only then is the temporary cache file removed. This avoids platform-specific “file in use” problems and makes the class safe to reuse.

Conclusion

TMSCityGuide illustrates several broadly useful FireMonkey techniques:

  • Isolate native platform APIs behind a common Delphi wrapper
  • Use conditional compilation inside implementation units rather than throughout the UI
  • Use secure sharing mechanisms such as Android FileProvider
  • Use temporary and cache directories through System.IOUtils
  • Handle asynchronous AI responses with events
  • Adapt stream-based data to filename-based platform APIs
  • Carefully manage native resources before deleting temporary files.

The result is application code that remains simple and readable while still integrating deeply with Android, iOS, cloud AI services, and device hardware.

Everything is open source

Like our previous summer projects, the complete source code is available for free. Clone it, learn from it, extend it, or use parts of it in your own applications.

The full project is available on GitHub:

GitHub repository

Built with TMS components

TMS City Guide showcases how easy it is to combine modern AI capabilities with native cross-platform Delphi development.

The application was built using:

To celebrate this summer project, we're offering a 25% discount on these products until August 31, 2026.

25% Discount

Simply use coupon code Coupon code during checkout to activate the discount.



Bruno Fierens


  1. Build Your Own AI-Powered Summer City Guide in Delphi

  2. From Delphi Code to City: Our TMS AI City Guide Explores Bruges

  3. A technical look at the TMS AI City Guide summer project



This blog post has not received any comments yet. Add a comment.



All Blog Posts  |  Next Post  |  Previous Post